From 8b61e88c0a58d5fe6e59b56a2f238f2fda87399c Mon Sep 17 00:00:00 2001 From: Hsy <32729842@qq.com> Date: Sat, 14 Feb 2026 11:04:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composeApp/build.gradle.kts | 27 +- .../src/androidMain/AndroidManifest.xml | 19 +- .../kotlin/com/taskttl/MainActivity.kt | 6 +- .../taskttl/core/utils/DeviceUtils.android.kt | 7 +- .../taskttl/core/utils/ToastUtils.android.kt | 7 + .../domain/repository/AuthRepositoryImpl.kt | 6 +- .../drawable/ic_empty_data.xml | 21 ++ .../drawable/ic_empty_error.xml | 12 + .../drawable/ic_empty_network.xml | 15 + .../composeResources/drawable/ic_error.xml | 26 ++ .../composeResources/values/strings.xml | 7 + .../commonMain/kotlin/com/taskttl/app/App.kt | 3 +- .../kotlin/com/taskttl/app/di/KoinModels.kt | 48 ++- .../com/taskttl/core/auth/AuthManager.kt | 128 +++++++ .../kotlin/com/taskttl/core/base/BaseState.kt | 8 +- .../com/taskttl/core/base/BaseViewModel.kt | 10 + .../core/config/FlattenBaseReqSerializer.kt | 4 + .../core/designsystem/component/Spacer.kt | 141 ++++++++ .../taskttl/core/designsystem/theme/Color.kt | 306 ++++++++++++++++ .../taskttl/core/designsystem/theme/Shape.kt | 83 +++++ .../taskttl/core/designsystem/theme/Size.kt | 117 ++++++ .../taskttl/core/designsystem/theme/Theme.kt | 106 ++++++ .../taskttl/core/designsystem/theme/Type.kt | 195 ++++++++++ .../kotlin/com/taskttl/core/domain/BaseReq.kt | 2 + .../com/taskttl/core/network/ApiConfig.kt | 9 +- .../taskttl/core/network/AuthHttpClient.kt | 28 ++ .../com/taskttl/core/network/KtorClient.kt | 69 +++- .../core/ui/component/divider/Divider.kt | 19 + .../taskttl/core/ui/component/empty/Empty.kt | 121 +++++++ .../core/ui/component/empty/EmptyData.kt | 36 ++ .../core/ui/component/empty/EmptyError.kt | 49 +++ .../core/ui/component/empty/EmptyNetwork.kt | 48 +++ .../com/taskttl/core/utils/StorageUtils.kt | 51 ++- .../com/taskttl/data/constant/Constant.kt | 7 + .../repository/CountdownRepositoryImpl.kt | 11 + .../data/repository/TaskRepositoryImpl.kt | 14 + .../data/source/local/dao/CategoryDao.kt | 10 +- .../data/source/local/dao/CountdownDao.kt | 7 + .../taskttl/data/source/local/dao/TaskDao.kt | 6 + .../data/source/remote/api/TaskTTLApi.kt | 41 ++- .../source/remote/dto/request/LoginReq.kt | 24 -- .../remote/dto/request/RefreshTokenRequest.kt | 16 + .../remote/dto/request/ThirdPartyLoginReq.kt | 39 ++ .../source/remote/dto/response/AuthResult.kt | 6 + .../dto/response/RefreshTokenResponse.kt | 32 ++ .../dto/response/ThirdPartyLoginResp.kt | 55 +++ .../com/taskttl/domain/model/UserInfo.kt | 21 ++ .../domain/repository/CountdownRepository.kt | 10 + .../domain/repository/TaskRepository.kt | 10 + .../kotlin/com/taskttl/navigation/MainNav.kt | 48 ++- .../kotlin/com/taskttl/navigation/Routes.kt | 32 +- .../common/components/ActionButtonListItem.kt | 4 +- .../common/components/NetworkImage.kt | 9 + .../common/foundation/GlobalImageLoader.kt | 3 + .../presentation/common/theme/Theme.kt | 139 ------- .../taskttl/presentation/common/theme/Type.kt | 40 --- .../features/auth/AuthViewModel.kt | 90 ----- .../features/category/list/CategoryScreen.kt | 26 +- .../features/category/list/CategoryState.kt | 17 +- .../countdown/list/CountdownScreen.kt | 22 +- .../features/countdown/list/CountdownState.kt | 16 +- .../features/onboarding/OnboardingScreen.kt | 2 +- .../features/onboarding/OnboardingState.kt | 2 +- .../onboarding/OnboardingViewModel.kt | 2 +- .../features/settings/about/AboutScreen.kt | 11 +- .../dataManagement/DataManagementScreen.kt | 339 +++++------------- .../dataManagement/DataManagementState.kt | 61 ++++ .../dataManagement/DataManagementViewModel.kt | 62 ++++ .../components/ConfirmDialog.kt | 53 +++ .../components/DataManagementCard.kt | 92 +++++ .../components/ExportDataDialog.kt | 81 +++++ .../components/ImportDataDialog.kt | 68 ++++ .../settings/feedback/FeedbackScreen.kt | 7 +- .../settings/feedback/FeedbackState.kt | 11 +- .../features/settings/main/SettingsScreen.kt | 40 ++- .../features/settings/main/SettingsState.kt | 5 +- .../settings/main/SettingsViewModel.kt | 10 +- .../settings/main/common/UserInfoCard.kt | 35 +- .../settings/privacy/PrivacyScreen.kt | 6 +- .../features/splash/SplashScreen.kt | 11 +- .../features/splash/SplashState.kt | 2 +- .../features/splash/SplashViewModel.kt | 10 +- .../features/statistics/StatisticsScreen.kt | 165 +-------- .../components/CategoryStatisticItem.kt | 134 +++++++ .../statistics/components/StatisticCard.kt | 76 ++++ .../features/task/list/TaskScreen.kt | 23 +- .../features/task/list/TaskState.kt | 13 +- .../{auth => user/login}/LoginScreen.kt | 177 ++++----- .../AuthState.kt => user/login/LoginState.kt} | 34 +- .../features/user/login/LoginViewModel.kt | 104 ++++++ .../user/userProfile/UserProfileScreen.kt | 241 +++++++++++++ .../user/userProfile/UserProfileState.kt | 55 +++ .../user/userProfile/UserProfileViewModel.kt | 37 ++ .../user/userProfile/components/AvatarView.kt | 66 ++++ .../user/userProfile/components/EmailRow.kt | 41 +++ .../user/userProfile/components/InfoRow.kt | 57 +++ .../userProfile/components/NicknameEditor.kt | 55 +++ .../kotlin/com/taskttl/MainViewController.kt | 1 + gradle/libs.versions.toml | 57 ++- 99 files changed, 3689 insertions(+), 1046 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_error.xml create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt rename composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/{auth => user/login}/LoginScreen.kt (53%) rename composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/{auth/AuthState.kt => user/login/LoginState.kt} (57%) create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 1ced809..862ad92 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -49,7 +49,7 @@ kotlin { sourceSets { androidMain.dependencies { - implementation(compose.preview) + implementation(libs.jetbrains.ui.tooling.preview) implementation(libs.androidx.activity.compose) // 启动 @@ -70,8 +70,8 @@ kotlin { // sqlite implementation(libs.androidx.room.sqlite.wrapper) - // admob - implementation(libs.android.play.services.ads.identifier) + // ads identifier + implementation(libs.androidx.ads.identifier) // work implementation(libs.androidx.work) @@ -84,12 +84,12 @@ kotlin { implementation(libs.androidx.login.facebook) } commonMain.dependencies { - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) + implementation(libs.runtime) + implementation(libs.foundation) + implementation(libs.material3) + implementation(libs.ui) + implementation(libs.components.resources) + implementation(libs.jetbrains.ui.tooling.preview) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.savedstate) implementation(libs.androidx.lifecycle.runtimeCompose) @@ -97,11 +97,14 @@ kotlin { // 导航 implementation(libs.navigation.compose) + // implementation(libs.androidx.navigation3.ui) + // implementation(libs.androidx.navigation3.material3.adaptive) // Koin依赖注入 implementation(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.viewmodel) + implementation(libs.koin.navigation3) // Ktor网络请求 implementation(libs.ktor.client.core) @@ -113,7 +116,7 @@ kotlin { implementation(libs.coil3.compose) implementation(libs.coil3.svg) // implementation(libs.coil3.gif) - // implementation(libs.coil3.network.ktor3) + implementation(libs.coil3.network.ktor3) // 添加日期时间处理依赖 implementation(libs.kotlinx.datetime) @@ -162,7 +165,7 @@ android { minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = libs.versions.android.versionCode.get().toInt() - versionName = libs.versions.android.versionName.get().toString() + versionName = libs.versions.android.versionName.get() buildConfigField("String", "APP_NAME", "\"TaskTTL\"") @@ -206,7 +209,7 @@ android { } dependencies { - debugImplementation(compose.uiTooling) + debugImplementation(libs.ui.tooling) // add("kspCommonMainMetadata",libs.androidx.room.compiler) // add("kspCommonMain",libs.androidx.room.compiler) // add("kspWasmJs",libs.androidx.room.compiler) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 264168c..4020f07 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -14,19 +14,25 @@ + + + + @@ -41,10 +47,9 @@ - + + + @@ -70,7 +75,7 @@ android:name=".core.receiver.AlarmReceiver" android:enabled="true" android:exported="true" - > + android:permission="com.taskttl.permission.ALARM"> diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt index 61d1fa8..5166e52 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt @@ -14,9 +14,9 @@ import com.taskttl.app.App class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { // ✅ Android 12+ 启动系统原生 Splash - val splashScreen = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - installSplashScreen() - } else null + // val splashScreen = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // installSplashScreen() + // } else null // splashScreen?.apply { // // setKeepOnScreenCondition { isChecking } // } diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt index 13a26b6..2772d9e 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt @@ -69,6 +69,7 @@ actual object DeviceUtils { uniqueId = getUniqueId(), deviceInfo = Build.MODEL ?: "0", deviceVersion = Build.VERSION.RELEASE, + country = getSimOrNetworkCountry(), language = Localization.getDeviceLanguage() ) } @@ -97,6 +98,7 @@ actual object DeviceUtils { */ private fun isValidAdId(adId: String?): Boolean { if (adId.isNullOrBlank()) return false + // Regex("^0{8}-0{4}-0{4}-0{4}-0{12}$") if (adId == "00000000-0000-0000-0000-000000000000") return false if (adId.matches(Regex("^0+$"))) return false return true @@ -119,9 +121,10 @@ actual object DeviceUtils { fun getSimOrNetworkCountry(): String { val tm = appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + return when { - !tm.networkCountryIso.isNullOrEmpty() -> tm.networkCountryIso.uppercase() - !tm.simCountryIso.isNullOrEmpty() -> tm.simCountryIso.uppercase() + !tm.networkCountryIso.isNullOrBlank() -> tm.networkCountryIso.uppercase() + !tm.simCountryIso.isNullOrBlank() -> tm.simCountryIso.uppercase() else -> "" } } diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt index afbaedd..76fed67 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt @@ -7,4 +7,11 @@ actual object ToastUtils { actual fun show(message: String) { Toast.makeText(MainApplication.instance.applicationContext, message, Toast.LENGTH_SHORT).show() } + + fun showWarning(message: String) { + // val params = ToastParams() + // params.text = text + // params.style = CustomToastStyle(R.layout.toast_warn) + // Toaster.show(params) + } } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt b/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt index 8983421..d4f7bfc 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt @@ -66,11 +66,9 @@ class AuthRepositoryImpl() : AuthRepository { AuthResult.Canceled } } catch (e: GetCredentialException) { - e.message?.let { LogUtils.e("DevTTL", it) } - AuthResult.Error("获取凭据失败: ${e.message}") + AuthResult.Error("${e.message}") } catch (e: Exception) { - e.message?.let { LogUtils.e("DevTTL", it) } - AuthResult.Error("Google 登录失败: ${e.message}") + AuthResult.Error(" ${e.message}") } } diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml b/composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml new file mode 100644 index 0000000..b684010 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml b/composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml new file mode 100644 index 0000000..b0c731d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml @@ -0,0 +1,12 @@ + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml b/composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml new file mode 100644 index 0000000..0fe4c49 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_error.xml b/composeApp/src/commonMain/composeResources/drawable/ic_error.xml new file mode 100644 index 0000000..ae4a653 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_error.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 3b39e6e..207003d 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -316,4 +316,11 @@ EEE, MMM d, yyyy h:mm a + + 暂无数据 + 加载失败 + 页面加载出现了问题 + 网络连接失败 + 请检查网络连接后重试 + 重新加载 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt index 5039f33..2a06b3c 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt @@ -4,10 +4,11 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import com.taskttl.core.designsystem.theme.AppTheme import com.taskttl.core.manager.ThemeMode import com.taskttl.navigation.AppNav import com.taskttl.presentation.common.foundation.GlobalImageLoader -import com.taskttl.ui.theme.AppTheme + import org.koin.compose.viewmodel.koinViewModel diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt index da52387..ccbe14c 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt @@ -6,15 +6,18 @@ import com.taskttl.data.repository.OnboardingRepositoryImpl import com.taskttl.data.repository.SettingsRepositoryImpl import com.taskttl.domain.repository.OnboardingRepository import com.taskttl.domain.repository.SettingsRepository -import com.taskttl.presentation.features.auth.AuthViewModel import com.taskttl.presentation.features.category.list.CategoryViewModel import com.taskttl.presentation.features.countdown.list.CountdownViewModel import com.taskttl.presentation.features.onboarding.OnboardingViewModel +import com.taskttl.presentation.features.settings.dataManagement.DataManagementViewModel import com.taskttl.presentation.features.settings.feedback.FeedbackViewModel import com.taskttl.presentation.features.settings.main.SettingsViewModel import com.taskttl.presentation.features.splash.SplashViewModel import com.taskttl.presentation.features.task.list.TaskViewModel +import com.taskttl.presentation.features.user.login.LoginViewModel +import com.taskttl.presentation.features.user.userProfile.UserProfileViewModel import org.koin.core.KoinApplication +import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.context.startKoin import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.viewModelOf @@ -44,7 +47,6 @@ val repositoryModule = module { /** 视图模型模块 */ val viewModelModule = module { - viewModelOf(::AppViewModel) viewModelOf(::SplashViewModel) viewModelOf(::OnboardingViewModel) @@ -53,5 +55,45 @@ val viewModelModule = module { viewModelOf(::CountdownViewModel) viewModelOf(::SettingsViewModel) viewModelOf(::FeedbackViewModel) - viewModelOf(::AuthViewModel) + viewModelOf(::LoginViewModel) + viewModelOf(::DataManagementViewModel) + viewModelOf(::UserProfileViewModel) } + + +@OptIn(KoinExperimentalAPI::class) +val appModule = module { + // navigation { + // + // + // MainNav() + // // SplashScreen(navigatorToRoute = { + // // globalNavController.navigate(it) { + // // popUpTo { inclusive = true } + // // // 确保MainScreen是单例的 + // // launchSingleTop = true + // // } + // // }) + // } + + + // composable { + // SplashScreen(navigatorToRoute = { + // globalNavController.navigate(it) { + // popUpTo { inclusive = true } + // // 确保MainScreen是单例的 + // launchSingleTop = true + // } + // }) + // } + // composable { + // OnboardingScreen(navigatorToRoute = { + // globalNavController.navigate(it) { + // popUpTo { inclusive = true } + // // 确保MainScreen是单例的 + // launchSingleTop = true + // } + // }) + // } + // composable { MainNav() } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt new file mode 100644 index 0000000..9a48358 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt @@ -0,0 +1,128 @@ +package com.taskttl.core.auth + +import com.taskttl.core.utils.StorageUtils +import com.taskttl.data.constant.Constant +import com.taskttl.data.source.remote.api.TaskTTLApi +import com.taskttl.data.source.remote.dto.request.RefreshTokenRequest +import com.taskttl.domain.model.UserInfo +import io.ktor.client.HttpClient +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.Serializable +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +/** + * 令牌信息 + * @author admin + * @date 2025/11/23 + * @constructor 创建[TokenInfo] + * @param [accessToken] 访问令牌 + * @param [refreshToken] 刷新令牌 + * @param [expiresAt] 到期日 + */ +@Serializable +data class TokenInfo( + val accessToken: String, // 访问令牌 + val refreshToken: String, // 刷新令牌 + val expiresAt: Long, // 过期时间,毫秒时间戳 +) + +/** + * 负责管理 Token、判断是否过期、刷新 Token + */ +object AuthManager { + + private val _tokenInfo = + MutableStateFlow(StorageUtils.getObject(Constant.KEY_TOKEN_INFO)) + + private val _userInfo = + MutableStateFlow(StorageUtils.getObject(Constant.KEY_USER_INFO)) + + val userInfo: StateFlow = _userInfo + + private val mutex = Mutex() + + // 预留一个刷新提前量(比如提前 1 分钟刷新) + private const val EXPIRE_EARLY_MS = 60_000L + + /** + * 更新令牌 + * @param [info] 信息 + */ + fun updateToken(info: TokenInfo?) { + _tokenInfo.value = info + if (info == null) { + StorageUtils.remove(Constant.KEY_TOKEN_INFO) + } else { + StorageUtils.saveObject(Constant.KEY_TOKEN_INFO, info) + } + } + + fun updateUserInfo(info: UserInfo?) { + _userInfo.value = info + if (info == null) { + StorageUtils.remove(Constant.KEY_USER_INFO) + } else { + StorageUtils.saveObject(Constant.KEY_USER_INFO, info) + } + } + + fun clear() { + updateToken(null) + updateUserInfo(null) + } + + fun currentAccessToken(): String? = _tokenInfo.value?.accessToken + + /** + * 已过期 + * @return [Boolean] + */ + @OptIn(ExperimentalTime::class) + fun isExpired(): Boolean { + val info = _tokenInfo.value ?: return true + val now = Clock.System.now().toEpochMilliseconds() + return now >= (info.expiresAt - EXPIRE_EARLY_MS) + } + + /** + * 对外暴露:获取一个“确保可用”的 AccessToken + * - 如果没过期:直接返回 + * - 如果过期:刷新后返回 + */ + suspend fun getValidAccessToken(): String? { + return mutex.withLock { + // double check,避免并发重复刷新 + if (!isExpired()) { + return _tokenInfo.value?.accessToken + } + // 已过期,尝试刷新 + if (tryRefreshToken()) { + return _tokenInfo.value?.accessToken + } + null + } + } + + /** + * 尝试刷新 Token,成功返回 true + */ + @OptIn(ExperimentalTime::class) + suspend fun tryRefreshToken(): Boolean { + val info = _tokenInfo.value ?: return false + if (info.refreshToken.isBlank()) return false + + return try { + val refreshToken = RefreshTokenRequest(refreshToken = info.refreshToken) + val result = TaskTTLApi.refreshToken(refreshToken) + updateToken(result.toTokenInfo()) + true + } catch (_: Exception) { + // 刷新失败可以视情况 clear 或保留老的 + false + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt index 6d1541a..6893af6 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt @@ -13,4 +13,10 @@ open class BaseState( open val isLoading: Boolean = false, open val isProcessing: Boolean = false, open val error: String? = null, -) \ No newline at end of file +) + +data class ProcessingState( + override val isLoading: Boolean = true, // 是否正在加载 + override val isProcessing: Boolean = true, // 是否正在处理 + override val error: String? = null // 错误信息 +) : BaseState(isLoading, isProcessing, error) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt index a512711..886c459 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt @@ -2,6 +2,8 @@ package com.taskttl.core.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.taskttl.core.utils.DeviceUtils +import com.taskttl.core.utils.LogUtils import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -79,5 +81,13 @@ abstract class BaseViewModel(initialState: S) : ViewModel() // protected fun clearError() { // _state.value = _state.value.copy(error = null) // } + + /** + * 日志调试 + * @param [message] 消息 + */ + protected fun logDebug(message: String) { + LogUtils.d("DevTTL_" + this::class.simpleName.toString(), message) + } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt index b19a536..d9f6dec 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt @@ -27,6 +27,7 @@ class FlattenBaseReqSerializer( element("uniqueId", PrimitiveSerialDescriptor("uniqueId", PrimitiveKind.STRING)) element("deviceInfo", PrimitiveSerialDescriptor("deviceInfo", PrimitiveKind.STRING)) element("deviceVersion", PrimitiveSerialDescriptor("deviceVersion", PrimitiveKind.STRING)) + element("country",PrimitiveSerialDescriptor("country", PrimitiveKind.STRING)) element("language", PrimitiveSerialDescriptor("language", PrimitiveKind.STRING)) } @@ -42,6 +43,7 @@ class FlattenBaseReqSerializer( jsonObj["uniqueId"] = JsonPrimitive(base.uniqueId) jsonObj["deviceInfo"] = JsonPrimitive(base.deviceInfo) jsonObj["deviceVersion"] = JsonPrimitive(base.deviceVersion) + jsonObj["country"] = JsonPrimitive(base.country) jsonObj["language"] = JsonPrimitive(base.language) jsonEncoder.encodeJsonElement(JsonObject(jsonObj)) @@ -59,6 +61,7 @@ class FlattenBaseReqSerializer( uniqueId = jsonObj["uniqueId"]!!.jsonPrimitive.content, deviceInfo = jsonObj["deviceInfo"]!!.jsonPrimitive.content, deviceVersion = jsonObj["deviceVersion"]!!.jsonPrimitive.content, + country = jsonObj["country"]!!.jsonPrimitive.content, language = jsonObj["language"]!!.jsonPrimitive.content ) @@ -69,6 +72,7 @@ class FlattenBaseReqSerializer( jsonObj.remove("uniqueId") jsonObj.remove("deviceInfo") jsonObj.remove("deviceVersion") + jsonObj.remove("country") jsonObj.remove("language") // 反序列化剩下的业务字段 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt new file mode 100644 index 0000000..8d8b7a8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt @@ -0,0 +1,141 @@ +package com.taskttl.core.designsystem.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.taskttl.core.designsystem.theme.SpaceHorizontalMedium +import com.taskttl.core.designsystem.theme.SpaceHorizontalSmall +import com.taskttl.core.designsystem.theme.SpaceHorizontalXLarge +import com.taskttl.core.designsystem.theme.SpaceHorizontalXSmall +import com.taskttl.core.designsystem.theme.SpaceHorizontalXXLarge +import com.taskttl.core.designsystem.theme.SpaceVerticalLarge +import com.taskttl.core.designsystem.theme.SpaceVerticalMedium +import com.taskttl.core.designsystem.theme.SpaceVerticalSmall +import com.taskttl.core.designsystem.theme.SpaceVerticalXLarge +import com.taskttl.core.designsystem.theme.SpaceVerticalXSmall +import com.taskttl.core.designsystem.theme.SpaceVerticalXXLarge + + +/** + * 创建一个超大垂直间距(32dp)的Spacer组件 + * 使用方式:SpaceVerticalXXLarge() + + */ +@Composable +fun SpaceVerticalXXLarge() { + Spacer(modifier = Modifier.height(SpaceVerticalXXLarge)) +} + +/** + * 创建一个特大垂直间距(24dp)的Spacer组件 + * 使用方式:SpaceVerticalXLarge() + + */ +@Composable +fun SpaceVerticalXLarge() { + Spacer(modifier = Modifier.height(SpaceVerticalXLarge)) +} + +/** + * 创建一个大垂直间距(16dp)的Spacer组件 + * 使用方式:SpaceVerticalLarge() + + */ +@Composable +fun SpaceVerticalLarge() { + Spacer(modifier = Modifier.height(SpaceVerticalLarge)) +} + +/** + * 创建一个中等垂直间距(12dp)的Spacer组件 + * 使用方式:SpaceVerticalMedium() + + */ +@Composable +fun SpaceVerticalMedium() { + Spacer(modifier = Modifier.height(SpaceVerticalMedium)) +} + +/** + * 创建一个小垂直间距(8dp)的Spacer组件 + * 使用方式:SpaceVerticalSmall() + + */ +@Composable +fun SpaceVerticalSmall() { + Spacer(modifier = Modifier.height(SpaceVerticalSmall)) +} + +/** + * 创建一个超小垂直间距(4dp)的Spacer组件 + * 使用方式:SpaceVerticalXSmall() + + */ +@Composable +fun SpaceVerticalXSmall() { + Spacer(modifier = Modifier.height(SpaceVerticalXSmall)) +} +// endregion + +// region 水平间距组件 +/** + * 创建一个超大水平间距(32dp)的Spacer组件 + * 使用方式:SpaceHorizontalXXLarge() + + */ +@Composable +fun SpaceHorizontalXXLarge() { + Spacer(modifier = Modifier.width(SpaceHorizontalXXLarge)) +} + +/** + * 创建一个特大水平间距(24dp)的Spacer组件 + * 使用方式:SpaceHorizontalXLarge() + + */ +@Composable +fun SpaceHorizontalXLarge() { + Spacer(modifier = Modifier.width(SpaceHorizontalXLarge)) +} + +/** + * 创建一个大水平间距(16dp)的Spacer组件 + * 使用方式:SpaceHorizontalLarge() + + */ +@Composable +fun SpaceHorizontalLarge() { + Spacer(modifier = Modifier.width(SpaceHorizontalXLarge)) +} + +/** + * 创建一个中等水平间距(12dp)的Spacer组件 + * 使用方式:SpaceHorizontalMedium() + + */ +@Composable +fun SpaceHorizontalMedium() { + Spacer(modifier = Modifier.width(SpaceHorizontalMedium)) +} + +/** + * 创建一个小水平间距(8dp)的Spacer组件 + * 使用方式:SpaceHorizontalSmall() + + */ +@Composable +fun SpaceHorizontalSmall() { + Spacer(modifier = Modifier.width(SpaceHorizontalSmall)) +} + +/** + * 创建一个超小水平间距(4dp)的Spacer组件 + * 使用方式:SpaceHorizontalXSmall() + + */ +@Composable +fun SpaceHorizontalXSmall() { + Spacer(modifier = Modifier.width(SpaceHorizontalXSmall)) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt new file mode 100644 index 0000000..8b52e4f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt @@ -0,0 +1,306 @@ +package com.taskttl.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + + +/** + * 颜色规范 + * 定义应用程序中使用的所有颜色,包括浅色和深色主题 + */ + +/** + * 品牌主色(普通模式):#465CFF + * 场景:菜单栏、主要按钮、突出文字 + */ +val PrimaryLight = Color(0xFF465CFF) + +/** + * 品牌主色(暗黑模式):#466CFF + * 场景:暗黑主题下的菜单栏、主要按钮、突出文字 + */ +val PrimaryDark = Color(0xFF466CFF) + +/** + * 品牌主色 + * 暂时和亮色模式一致可自行修改 + */ +val Primary = PrimaryLight + +// 辅助色 +/** + * 危险色/红色:#FF2B2B + * 适用场景:错误提示、删除操作、警告信息等 + */ +val ColorDanger = Color(0xFFFF2B2B) // 危险色/红色 + +/** + * 深色模式危险色/红色:#FF2B3B + */ +val ColorDangerDark = Color(0xFFFF2B3B) + +/** + * 警告色/黄色:#FFB703 + * 适用场景:警告提示、需要注意的信息等 + */ +val ColorWarning = Color(0xFFFFB703) // 警告色/黄色 + +/** + * 深色模式警告色/黄色:#FFB704 + */ +val ColorWarningDark = Color(0xFFFFB704) + +/** + * 紫色:#6831FF + * 适用场景:特殊强调、次要品牌色等 + */ +val ColorPurple = Color(0xFF6831FF) // 紫色 + +/** + * 深色模式紫色:#6832FF + */ +val ColorPurpleDark = Color(0xFF6832FF) + +/** + * 成功色/绿色:#09BE4F + * 适用场景:成功提示、完成状态等 + */ +val ColorSuccess = Color(0xFF09BE4F) // 成功色/绿色 + +/** + * 深色模式成功色/绿色:#09BE5F + */ +val ColorSuccessDark = Color(0xFF09BE5F) + +// 字体颜色 - 浅色模式 +/** + * 浅色模式下主要文字颜色:#181818 + * 适用场景:标题、重要文本内容 + */ +val TextPrimaryLight = Color(0xFF181818) // 用于重要标题内容 + +/** + * 浅色模式下次要文字颜色:#333333 + * 适用场景:正文内容、次要标题 + */ +val TextSecondaryLight = Color(0xFF333333) // 用于普通内容 + +/** + * 浅色模式下次要标题颜色:#7F7F7F + * 适用场景:表单标题、提示标签等 + */ +val TextSubtitleLight = Color(0xFF7F7F7F) + +/** + * 浅色模式下三级文字颜色:#B2B2B2 + * 适用场景:辅助说明、标签文字 + */ +val TextTertiaryLight = Color(0xFFB2B2B2) // 用于底部标签描述 + +/** + * 浅色模式下四级文字颜色:#CCCCCC + * 适用场景:次要辅助信息、禁用状态文字 + */ +val TextQuaternaryLight = Color(0xFFCCCCCC) // 用于辅助次要信息 + +/** + * 按钮文字:#FFFFFF(两种模式一致) + */ +val TextWhite = Color(0xFFFFFFFF) + +// 字体颜色 - 深色模式 +/** + * 深色模式下主要文字颜色:#D1D1D1 + * 适用场景:深色模式下的标题、重要文本内容 + */ +val TextPrimaryDark = Color(0xFFD1D1D1) // 深色模式下的主要文字 + +/** + * 深色模式下次要文字颜色:#A3A3A3 + * 适用场景:深色模式下的正文内容、次要标题 + */ +val TextSecondaryDark = Color(0xFFA3A3A3) // 深色模式下的次要文字 + +/** + * 深色模式下次要标题颜色:#8C8C8C + * 适用场景:深色模式下的表单标题、提示标签等 + */ +val TextSubtitleDark = Color(0xFF8C8C8C) + +/** + * 深色模式下三级文字颜色:#8D8D8D + * 适用场景:深色模式下的辅助说明、标签文字 + */ +val TextTertiaryDark = Color(0xFF8D8D8D) // 深色模式下的三级文字 + +/** + * 深色模式下四级文字颜色:#5E5E5E + * 适用场景:深色模式下的次要辅助信息、禁用状态文字 + */ +val TextQuaternaryDark = Color(0xFF5E5E5E) // 深色模式下的四级文字 + +// 背景色 - 浅色模式 +/** + * 浅色模式下页面背景色:#F1F4FA + * 适用场景:应用整体背景、页面底色 + */ +val BgGreyLight = Color(0xFFF1F4FA) // 页面背景底色 + +/** + * 浅色模式下白色背景:#FFFFFF + * 适用场景:卡片、弹窗等内容区域背景 + */ +val BgWhiteLight = Color(0xFFFFFFFF) // 白色背景 + +/** + * 浅色模式下内容模块背景色:#F8F8F8 + * 适用场景:次级内容区域、列表项底色 + */ +val BgContentLight = Color(0xFFF8F8F8) // 内容模块底色 + +/** + * 浅色模式下红色背景:#FF2B2B(5%透明度) + * 适用场景:红色主题的轻量化背景、提示区域 + */ +val BgRedLight = Color(0x0DFF2B2B) // 红色背景 5% 透明度 + +/** + * 浅色模式下黄色背景:#FFB703(10%透明度) + * 适用场景:黄色主题的轻量化背景、警告区域 + */ +val BgYellowLight = Color(0x1AFFB703) // 黄色背景 10% 透明度 + +/** + * 浅色模式下紫色背景:#6831FF(10%透明度) + * 适用场景:紫色主题的轻量化背景、特殊区域 + */ +val BgPurpleLight = Color(0x1A6831FF) // 紫色背景 10% 透明度 + +/** + * 浅色模式下绿色背景:#09BE4F(5%透明度) + * 适用场景:绿色主题的轻量化背景、成功提示区域 + */ +val BgGreenLight = Color(0x0D09BE4F) // 绿色背景 5% 透明度 + +// 背景色 - 深色模式 +/** + * 深色模式下页面背景色:#111111 + * 适用场景:深色模式下的应用整体背景、页面底色 + */ +val BgGreyDark = Color(0xFF111111) // 深色模式下的页面背景底色 + +/** + * 深色模式下白色背景:#1B1B1B + * 适用场景:深色模式下的卡片、弹窗等内容区域背景 + */ +val BgWhiteDark = Color(0xFF1B1B1B) // 深色模式下的白色背景 + +/** + * 深色模式下内容模块背景色:#222222 + * 适用场景:深色模式下的次级内容区域、列表项底色 + */ +val BgContentDark = Color(0xFF222222) // 深色模式下的内容模块底色 + +/** + * 深色模式下红色背景:#222222 + */ +val BgRedDark = Color(0xFF222222) + +/** + * 深色模式下黄色背景:#222222 + */ +val BgYellowDark = Color(0xFF222222) + +/** + * 深色模式下紫色背景:#222222 + */ +val BgPurpleDark = Color(0xFF222222) + +/** + * 深色模式下绿色背景:#222222 + */ +val BgGreenDark = Color(0xFF222222) + +// 遮罩颜色 +/** + * 浅色模式下遮罩颜色:60%透明度黑色 + * 适用场景:弹窗背景、加载状态遮罩 + */ +val MaskLight = Color(0x99000000) // rgba(0, 0, 0, 0.6) - 浅色模式 + +/** + * 深色模式下遮罩颜色:60%透明度黑色 + * 适用场景:深色模式下的弹窗背景、加载状态遮罩 + */ +val MaskDark = Color(0x99000000) // rgba(0, 0, 0, 0.6) - 深色模式 + +/** + * 浅色模式下按压状态颜色:20%透明度黑色 + * 适用场景:浅色模式下的按钮、卡片等组件的点击反馈 + */ +val PressLight = Color(0x33000000) // rgba(0, 0, 0, 0.2) - 浅色模式点击 + +/** + * 深色模式下按压状态颜色:20%透明度白色 + * 适用场景:深色模式下的按钮、卡片等组件的点击反馈 + */ +val PressDark = Color(0x33FFFFFF) // rgba(255, 255, 255, .2) - 深色模式点击 + +/** + * 浅色模式下轻量按压状态颜色:5%透明度黑色 + * 适用场景:弱态组件、背景较浅的点击反馈 + */ +val PressLightSoft = Color(0x0D000000) // rgba(0, 0, 0, 0.05) + +/** + * 深色模式下轻量按压状态颜色:10%透明度白色 + * 适用场景:弱态组件、背景较暗的点击反馈 + */ +val PressDarkSoft = Color(0x1AFFFFFF) // rgba(255, 255, 255, 0.1) + +/** + * 浅色模式下阴影颜色:#020426(5%透明度) + */ +val ShadowLight = Color(0x0D020426) + +/** + * 深色模式下阴影颜色:#111111(50%透明度) + */ +val ShadowDark = Color(0x80111111) + +// 边框颜色 +/** + * 浅色模式下边框颜色:#EEEEEE + * 适用场景:分割线、边框、描边等 + */ +val BorderLight = Color(0xFFEEEEEE) // 浅色模式边框 + +/** + * 深色模式下边框颜色:#242424 + * 适用场景:深色模式下的分割线、边框、描边等 + */ +val BorderDark = Color(0xFF242424) // 深色模式边框 + +// 渐变色起点和终点颜色 +/** + * 主色渐变起点:#465CFF + * 适用场景:与主色渐变终点配合使用,用于渐变按钮、背景等 + */ +val GradientPrimaryStart = Color(0xFF465CFF) // 主色渐变起点 + +/** + * 主色渐变终点:#6831FF + * 适用场景:与主色渐变起点配合使用,用于渐变按钮、背景等 + */ +val GradientPrimaryEnd = Color(0xFF6831FF) // 主色渐变终点 + +/** + * 红色渐变起点:#FD8C8C + * 适用场景:与红色渐变终点配合使用,用于警告类渐变效果 + */ +val GradientRedStart = Color(0xFFFD8C8C) // 红色渐变起点 + +/** + * 红色渐变终点:#FF2B2B + * 适用场景:与红色渐变起点配合使用,用于警告类渐变效果 + */ +val GradientRedEnd = Color(0xFFFF2B2B) // 红色渐变终点 \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt new file mode 100644 index 0000000..3e85860 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt @@ -0,0 +1,83 @@ +package com.taskttl.core.designsystem.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +/** + * 圆角规范 + */ + +// 标准圆角数值定义 + +/** + * 超小圆角数值:4dp + */ +val RadiusXSmall = 4.dp + +/** + * 小圆角数值:8dp + */ +val RadiusSmall = 8.dp + +/** + * 中圆角数值:12dp + */ +val RadiusMedium = 12.dp + +/** + * 大圆角数值:16dp + */ +val RadiusLarge = 16.dp + +/** + * 超大圆角数值:24dp + */ +val RadiusExtraLarge = 24.dp + +/** + * 超小圆角:4dp + * 适用场景:极小的UI元素,如图标按钮 + */ +val ShapeXSmall = RoundedCornerShape(RadiusXSmall) // 超小圆角 8px + +/** + * 小圆角:8dp + * 适用场景:常规卡片、按钮等小型UI元素的圆角 + */ +val ShapeSmall = RoundedCornerShape(RadiusSmall) // 小圆角 16px + +/** + * 中圆角:12dp + * 适用场景:中型容器、对话框等组件的圆角 + */ +val ShapeMedium = RoundedCornerShape(RadiusMedium) // 中圆角 24px + +/** + * 大圆角:16dp + * 适用场景:分类列表等组件 + */ +val ShapeLarge = RoundedCornerShape(RadiusLarge) // 大圆角 32px + +/** + * 超大圆角:24dp + * 适用场景:大型卡片、底部弹窗等较大UI元素的圆角 + */ +val ShapeExtraLarge = RoundedCornerShape(RadiusExtraLarge) // 超大圆角 48px + +/** + * 圆形 + * 适用场景:头像、图标按钮等需要完全圆形的组件 + */ +val ShapeCircle = RoundedCornerShape(percent = 50) + +/** + * Material3 Shapes配置 + * 将自定义圆角规范应用于Material3设计系统 + */ +val AppShapes = Shapes( + small = ShapeSmall, + medium = ShapeMedium, + large = ShapeExtraLarge, + extraLarge = ShapeExtraLarge +) diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt new file mode 100644 index 0000000..1f29175 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt @@ -0,0 +1,117 @@ +package com.taskttl.core.designsystem.theme + +import androidx.compose.ui.unit.dp + +/** + * 布局间距规范 + */ + +/** + * 超大垂直间距:32dp + * 适用场景:页面块与块之间的大间距,如不同功能模块之间的分隔 + */ +val SpaceVerticalXXLarge = 32.dp + +/** + * 特大垂直间距:24dp + * 适用场景:大模块内部的分隔,如卡片组之间的间距 + */ +val SpaceVerticalXLarge = 24.dp + +/** + * 大垂直间距:16dp + * 适用场景:卡片内部主要内容块之间的间距 + */ +val SpaceVerticalLarge = 16.dp + +/** + * 中等垂直间距:12dp + * 适用场景:内容区域的常规间距,如列表项之间的间距 + */ +val SpaceVerticalMedium = 12.dp + +/** + * 小垂直间距:8dp + * 适用场景:紧凑布局中的间距,如图标与文字之间 + */ +val SpaceVerticalSmall = 8.dp + +/** + * 超小垂直间距:4dp + * 适用场景:最小的垂直间隔,如紧凑排列的元素间距 + */ +val SpaceVerticalXSmall = 4.dp + +/** + * 超大水平间距:32dp + * 适用场景:页面左右边距,大型容器的内边距 + */ +val SpaceHorizontalXXLarge = 32.dp + +/** + * 特大水平间距:24dp + * 适用场景:大型容器内部的水平分隔 + */ +val SpaceHorizontalXLarge = 24.dp + +/** + * 大水平间距:16dp + * 适用场景:常规容器的左右边距,如卡片的内边距 + */ +val SpaceHorizontalLarge = 16.dp + +/** + * 中等水平间距:12dp + * 适用场景:中等容器的水平间距,如列表项的左右间距 + */ +val SpaceHorizontalMedium = 12.dp + +/** + * 小水平间距:8dp + * 适用场景:紧凑布局的水平间距,如图标与文字之间 + */ +val SpaceHorizontalSmall = 8.dp + +/** + * 超小平间间距:4dp + * 适用场景:最小的平间间隔,如紧凑排列的元素间距 + */ +val SpaceHorizontalXSmall = 4.dp + +/** + * 常用于内边距的值 + * 大内边距:16dp + * 适用场景:卡片、对话框等容器的内边距 + */ +val SpacePaddingLarge = 16.dp + +/** + * 中等内边距:12dp + * 适用场景:按钮、输入框等中型组件的内边距 + */ +val SpacePaddingMedium = 12.dp + +/** + * 小内边距:8dp + * 适用场景:紧凑型组件的内边距,如小型按钮 + */ +val SpacePaddingSmall = 8.dp + +/** + * 超小内边距:4dp + * 适用场景:最小的内边距,如标签、徽章等小组件 + */ +val SpacePaddingXSmall = 4.dp + +/** + * 其他常用尺寸 + * 分割线高度:0.5dp + * 适用场景:列表项之间的分割线、边框线等 + */ +val SpaceDivider = 0.5.dp // 分割线高度 + +/** + * 指示器高度:2dp + * 适用场景:选中指示器、进度条等 + */ +val SpaceIndicator = 2.dp // 指示器高度 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt new file mode 100644 index 0000000..b5cd981 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt @@ -0,0 +1,106 @@ +package com.taskttl.core.designsystem.theme +// 应用主题 +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +/** 浅色方案 - 聚焦效率与清晰度的 TaskTTL 品牌配色 */ +private val LightColorScheme = lightColorScheme( + primary = Color(0xFF3E63F5), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFDEE1FF), + onPrimaryContainer = Color(0xFF001465), + secondary = Color(0xFF0F8B8D), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFF99F0EE), + onSecondaryContainer = Color(0xFF00201F), + tertiary = Color(0xFFFFB155), + onTertiary = Color(0xFF432000), + tertiaryContainer = Color(0xFFFFDDB4), + onTertiaryContainer = Color(0xFF2B1200), + error = Color(0xFFB3261E), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFF9DEDC), + onErrorContainer = Color(0xFF410E0B), + background = Color(0xFFFAFCFF), + onBackground = Color(0xFF111428), + surface = Color(0xFFFFFFFF), + onSurface = Color(0xFF111428), + surfaceVariant = Color(0xFFE0E2EC), + onSurfaceVariant = Color(0xFF434654), + outline = Color(0xFF747680), + outlineVariant = Color(0xFFC4C6D2), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF2F3145), + inverseOnSurface = Color(0xFFF1F2F8), + inversePrimary = Color(0xFFB7C5FF), + surfaceDim = Color(0xFFD9DBE4), + surfaceBright = Color(0xFFF8F9FF), + surfaceContainerLowest = Color(0xFFFDFCFF), + surfaceContainerLow = Color(0xFFF3F4FA), + surfaceContainer = Color(0xFFEDF0FA), + surfaceContainerHigh = Color(0xFFE6E9F5), + surfaceContainerHighest = Color(0xFFE0E3F1) +) + +/** 深色方案 - OLED 友好且保持品牌温度 */ +private val DarkColorScheme = darkColorScheme( + primary = Color(0xFFB7C5FF), + onPrimary = Color(0xFF002178), + primaryContainer = Color(0xFF1D3277), + onPrimaryContainer = Color(0xFFDEE2FF), + secondary = Color(0xFF56D8D1), + onSecondary = Color(0xFF003734), + secondaryContainer = Color(0xFF00504D), + onSecondaryContainer = Color(0xFF99F0EE), + tertiary = Color(0xFFFFBA6C), + onTertiary = Color(0xFF452803), + tertiaryContainer = Color(0xFF653E0F), + onTertiaryContainer = Color(0xFFFFDDB4), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + background = Color(0xFF101323), + onBackground = Color(0xFFE2E3F1), + surface = Color(0xFF101323), + onSurface = Color(0xFFE2E3F1), + surfaceVariant = Color(0xFF434654), + onSurfaceVariant = Color(0xFFC4C6D2), + outline = Color(0xFF8E909C), + outlineVariant = Color(0xFF434654), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFE2E3F1), + inverseOnSurface = Color(0xFF26293A), + inversePrimary = Color(0xFF3E63F5), + surfaceDim = Color(0xFF101323), + surfaceBright = Color(0xFF363952), + surfaceContainerLowest = Color(0xFF090C19), + surfaceContainerLow = Color(0xFF181B2C), + surfaceContainer = Color(0xFF1C2032), + surfaceContainerHigh = Color(0xFF262A3D), + surfaceContainerHighest = Color(0xFF31354A) +) + +/** + * 应用主题 + * @param darkTheme 是否深色模式 + * @param content 页面内容 + */ +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + shapes = AppShapes, + content = content + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt new file mode 100644 index 0000000..65c9833 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt @@ -0,0 +1,195 @@ +package com.taskttl.core.designsystem.theme +// 字体规范 +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + + +/** + * 字体规范 + * 1px = 0.5sp 转换结果,未提供的尺寸按层级递减补齐 + */ +val Typography = Typography( + + /** + * 中粗体 · 22sp / 31sp + * 使用场景:超大标题,文章标题 + */ + displayLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, // 中粗体 + fontSize = 22.sp, + lineHeight = 31.sp, + letterSpacing = 0.sp + ), + + /** + * 中粗体 · 18sp / 27sp + * 使用场景:大标题 + */ + displayMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, // 中粗体 + fontSize = 18.sp, + lineHeight = 27.sp, + letterSpacing = 0.sp + ), + + /** + * 中粗体 · 16sp / 24sp + * 使用场景:展示级文案(中等长度) + */ + displaySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 16sp / 24sp + * 使用场景:二级标题、导航栏、列表、段落标题、按钮文字 + */ + headlineLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, // 中黑体 + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 13sp / 20sp + * 使用场景:信息分组小标题 + */ + headlineSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 13.sp, + lineHeight = 20.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 14sp / 22sp + * 使用场景:类别名称 + */ + headlineMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, // 中黑体 + fontSize = 14.sp, + lineHeight = 22.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 16sp / 20sp + * 使用场景:模块标题、弹窗标题 + */ + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 20.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 14sp / 20sp + * 使用场景:列表项标题、辅助性标题 + */ + titleMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 12sp / 18sp + * 使用场景:段落内小标题、二级描述 + */ + titleSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 18.sp, + letterSpacing = 0.sp + ), + + /** + * 常规体 · 14sp / 22sp + * 使用场景:正文内容、段落文字 + */ + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, // 常规体 + fontSize = 14.sp, + lineHeight = 22.sp, + letterSpacing = 0.sp + ), + + /** + * 常规体 · 12sp / 18sp + * 使用场景:底部导航栏文字、辅助性文字、标签文字 + */ + bodyMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, // 常规体 + fontSize = 12.sp, + lineHeight = 18.sp, + letterSpacing = 0.sp + ), + + /** + * 常规体 · 11sp / 16sp + * 使用场景:次级正文、辅助段落 + */ + bodySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.sp + ), + + /** + * 中黑体 · 12sp / 16sp + * 使用场景:按钮、标签等操作文字 + */ + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.1.sp + ), + + /** + * 中黑体 · 11sp / 16sp + * 使用场景:辅助标签、徽标说明 + */ + labelMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.1.sp + ), + + /** + * 中黑体 · 10sp / 14sp + * 使用场景:最小标签、角标 + */ + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 10.sp, + lineHeight = 14.sp, + letterSpacing = 0.1.sp + ) +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt index 903ffed..f9c2245 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt @@ -29,6 +29,7 @@ open class BaseReq( open val uniqueId: String, open val deviceInfo: String, open val deviceVersion: String, + open val country: String, open val language: String, ) @@ -54,5 +55,6 @@ fun defaultBaseReq(): BaseReq = BaseReq( uniqueId = "", deviceInfo = "", deviceVersion = "", + country = "", language = "" ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt index dc4582a..27921de 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt @@ -7,8 +7,8 @@ package com.taskttl.core.network */ object ApiConfig { /** 基本地址 */ - // const val BASE_URL = "http://10.0.0.5:8888/api/v1" - const val BASE_URL = "https://api.taskttl.com/api/v1" + const val BASE_URL = "https://test.svnet.cn/api/v1" + // const val BASE_URL = "https://api.taskttl.com/api/v1" /** 登录地址 */ const val LOGIN_URL = "$BASE_URL/login" @@ -16,9 +16,12 @@ object ApiConfig { /** 三方登录地址 */ const val THIRD_PARTY_LOGIN_URL = "$BASE_URL/thirdPartyLogin" + /** 刷新令牌url */ + const val REFRESH_TOKEN_URL = "$BASE_URL/auth/refreshToken" + /** 反馈地址 */ const val FEEDBACK_URL = "$BASE_URL/feedback" /** 埋点地址 */ const val POINT_URL = "$BASE_URL/point" -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt new file mode 100644 index 0000000..ff3b677 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt @@ -0,0 +1,28 @@ +package com.taskttl.core.network + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +object AuthHttpClient { + val client = HttpClient { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + encodeDefaults = false + explicitNulls = false + }) + } + + // 可以保留超时、日志等,但**不要**装 HttpSend 里那套 getValidAccessToken/401 刷新逻辑 + install(HttpTimeout) { + connectTimeoutMillis = 15000 + requestTimeoutMillis = 30000 + socketTimeoutMillis = 60000 + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt index ed9c290..f7b3d29 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt @@ -1,18 +1,25 @@ package com.taskttl.core.network +import com.taskttl.core.auth.AuthManager import com.taskttl.core.domain.ApiResponse import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.plugins.DefaultRequest import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.HttpSend import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.plugin +import io.ktor.client.request.header import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.ktor.http.userAgent import io.ktor.serialization.kotlinx.json.json @@ -40,6 +47,7 @@ object KtorClient { isLenient = true ignoreUnknownKeys = true // JSON多了字段也不报错 encodeDefaults = false + explicitNulls = false }) } @@ -66,6 +74,64 @@ object KtorClient { request.headers.append("X-Retry-Count", retryCount.toString()) } } + /** + * 默认请求: + * - 统一加 User-Agent + * - 统一加 Authorization(如果有 Token) + */ + install(DefaultRequest) { + userAgent("TaskTTL") + + val token = AuthManager.currentAccessToken() + if (!token.isNullOrBlank()) { + header(HttpHeaders.Authorization, "Bearer $token") + } + } + + install(HttpSend) { + maxSendCount = 2 + } + }.also { client -> + // ⭐ 这里配置 HttpSend 拦截器:401 自动刷新 + 重试 + client.plugin(HttpSend).intercept { request -> + + // 发送请求前,先确保 token 是有效的(过期则刷新) + val validToken = AuthManager.getValidAccessToken() + if (!validToken.isNullOrBlank()) { + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $validToken") + } + + // 先发一次请求 + var call = execute(request) + + // 如果不是 401,直接返回 + if (call.response.status != HttpStatusCode.Unauthorized) { + return@intercept call + } + + // 避免刷新接口自己 401 时死循环 + val url = request.url.toString() + if (url.contains(ApiConfig.REFRESH_TOKEN_URL)) { + return@intercept call + } + + // 尝试刷新 Token + val refreshed = AuthManager.tryRefreshToken() + if (!refreshed) { + return@intercept call + } + + // 刷新成功后,重发原请求 + val newToken = AuthManager.currentAccessToken() + if (!newToken.isNullOrBlank()) { + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $newToken") + } + + // 再发一次(第二次) + execute(request) + } } /** @@ -78,12 +144,11 @@ object KtorClient { try { val response = httpClient.post(url) { contentType(ContentType.Application.Json) - userAgent("TaskTTL") setBody(body) } return handleResponse(response) } catch (e: Exception) { - throw Exception(e) + throw e } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt new file mode 100644 index 0000000..c21384d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt @@ -0,0 +1,19 @@ +package com.taskttl.core.ui.component.divider + +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * 水平分割线组件 + * + * @param modifier 修饰符,用于自定义组件样式 + * @param color 分割线颜色,默认使用outline颜色 + */ +@Composable +fun Divider(modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.outline) { + HorizontalDivider(modifier, thickness = 0.5.dp, color = color) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt new file mode 100644 index 0000000..55e6cd0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt @@ -0,0 +1,121 @@ +package com.taskttl.core.ui.component.empty + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.taskttl.core.designsystem.component.SpaceVerticalSmall +import com.taskttl.core.designsystem.component.SpaceVerticalXLarge +import com.taskttl.core.designsystem.theme.SpacePaddingLarge +import com.taskttl.core.designsystem.theme.AppTheme +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.resources.vectorResource +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.click_retry +import taskttl.composeapp.generated.resources.empty_error +import taskttl.composeapp.generated.resources.ic_empty_error + +/** + * 页面状态视图 + * + * @param modifier 修饰符 + * @param message 消息文本资源ID + * @param subtitle 副标题文本资源ID + * @param retryButtonText 重试按钮文本资源ID + * @param icon 图标资源ID + * @param onRetryClick 重试点击回调 + + */ +@Composable +fun Empty( + modifier: Modifier = Modifier, + message: StringResource = Res.string.empty_error, + subtitle: StringResource? = null, + retryButtonText: StringResource = Res.string.click_retry, + icon: DrawableResource = Res.drawable.ic_empty_error, + onRetryClick: (() -> Unit)? = null, +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .padding(SpacePaddingLarge) + ) { + Icon( + imageVector = vectorResource(icon), + modifier = Modifier.size(120.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2F), + contentDescription = null, + ) + + SpaceVerticalXLarge() + + Text( + text = stringResource(message), + style = MaterialTheme.typography.titleLarge + ) + + subtitle?.let { + SpaceVerticalSmall() + Text( + text = stringResource(it), + style = MaterialTheme.typography.bodyMedium, + ) + } + + // 如果没有传递重试方法,则不显示重试按钮 + if (onRetryClick != null) { + SpaceVerticalXLarge() + OutlinedButton( + onClick = onRetryClick, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.primary), + modifier = Modifier + .padding(horizontal = 50.dp) + .widthIn(200.dp) + ) { + Text( + text = stringResource(retryButtonText) + ) + } + } + } +} + +/** + * 页面状态视图浅色主题预览 + * + */ +@Preview(showBackground = true) +@Composable +fun EmptyPreview() { + AppTheme { + Empty() + } +} + +/** + * 页面状态视图深色主题预览 + * + */ +@Preview(showBackground = true) +@Composable +fun EmptyPreviewDark() { + AppTheme(darkTheme = true) { + Empty() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt new file mode 100644 index 0000000..2a69269 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt @@ -0,0 +1,36 @@ +package com.taskttl.core.ui.component.empty + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.taskttl.core.designsystem.theme.AppTheme +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.click_retry +import taskttl.composeapp.generated.resources.empty_data +import taskttl.composeapp.generated.resources.ic_empty_data +import taskttl.composeapp.generated.resources.text_add_task_hint +import taskttl.composeapp.generated.resources.text_no_tasks + +@Composable +fun EmptyData(modifier: Modifier, onRetryClick: (() -> Unit)? = null) { + Empty( + message = Res.string.text_no_tasks, + subtitle = Res.string.text_add_task_hint + ) +} + +/** + * 暂无数据状态预览 + * + */ +@Preview(showBackground = true) +@Composable +fun EmptyDataPreview() { + AppTheme { + Empty( + message = Res.string.empty_data, + icon = Res.drawable.ic_empty_data, + retryButtonText = Res.string.click_retry, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt new file mode 100644 index 0000000..2a193fe --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt @@ -0,0 +1,49 @@ +package com.taskttl.core.ui.component.empty + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.taskttl.core.designsystem.theme.AppTheme +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.click_retry +import taskttl.composeapp.generated.resources.empty_error +import taskttl.composeapp.generated.resources.empty_error_subtitle +import taskttl.composeapp.generated.resources.ic_empty_error + +/** + * 加载失败状态视图 + * + * @param modifier 修饰符 + * @param onRetryClick 重试点击回调 + */ +@Composable +fun EmptyError( + modifier: Modifier = Modifier, + onRetryClick: (() -> Unit)? = null, +) { + Empty( + modifier = modifier, + message = Res.string.empty_error, + subtitle = Res.string.empty_error_subtitle, + icon = Res.drawable.ic_empty_error, + retryButtonText = Res.string.click_retry, + onRetryClick = onRetryClick + ) +} + +/** + * 加载失败状态预览 + * + + */ +@Preview(showBackground = true) +@Composable +fun EmptyErrorPreview() { + AppTheme { + Empty( + message = Res.string.empty_error, + icon = Res.drawable.ic_empty_error, + retryButtonText = Res.string.click_retry, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt new file mode 100644 index 0000000..e0eb2ba --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt @@ -0,0 +1,48 @@ +package com.taskttl.core.ui.component.empty + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.taskttl.core.designsystem.theme.AppTheme +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.click_retry +import taskttl.composeapp.generated.resources.empty_network +import taskttl.composeapp.generated.resources.empty_network_subtitle +import taskttl.composeapp.generated.resources.ic_empty_network + +/** + * 网络连接失败状态视图 + * + * @param modifier 修饰符 + * @param onRetryClick 重试点击回调 + */ +@Composable +fun EmptyNetwork( + modifier: Modifier = Modifier, + onRetryClick: (() -> Unit)? = null, +) { + Empty( + modifier = modifier, + message = Res.string.empty_network, + subtitle = Res.string.empty_network_subtitle, + icon = Res.drawable.ic_empty_network, + retryButtonText = Res.string.click_retry, + onRetryClick = onRetryClick + ) +} + +/** + * 网络连接失败状态预览 + * + */ +@Preview(showBackground = true) +@Composable +fun EmptyNetworkPreview() { + AppTheme { + Empty( + message = Res.string.empty_network, + icon = Res.drawable.ic_empty_network, + retryButtonText = Res.string.click_retry, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt index 6373cf4..08b5579 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt @@ -2,8 +2,7 @@ package com.taskttl.core.utils import com.russhwolf.settings.Settings import com.russhwolf.settings.contains -import com.russhwolf.settings.get -import com.russhwolf.settings.set +import kotlinx.serialization.json.Json /** @@ -13,14 +12,19 @@ import com.russhwolf.settings.set */ object StorageUtils { val settings: Settings = Settings() + + val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + } + /** * 保存字符串值 * @param key 键 * @param value 值 */ - fun saveString(key: String, value: String) { - settings.putString(key,value) - } + fun saveString(key: String, value: String) = settings.putString(key, value) /** * 获取字符串值 @@ -28,18 +32,14 @@ object StorageUtils { * @param defaultValue 默认值 * @return 存储的字符串值或默认值 */ - fun getString(key: String, defaultValue: String = ""): String { - return settings.getString(key,defaultValue) - } + fun getString(key: String, defaultValue: String = "") = settings.getString(key, defaultValue) /** * 保存整数值 * @param key 键 * @param value 值 */ - fun saveInt(key: String, value: Int) { - settings.putInt(key,value) - } + fun saveInt(key: String, value: Int) = settings.putInt(key, value) /** * 获取整数值 @@ -47,9 +47,7 @@ object StorageUtils { * @param defaultValue 默认值 * @return 存储的整数值或默认值 */ - fun getInt(key: String, defaultValue: Int = 0): Int { - return settings.getInt(key,defaultValue) - } + fun getInt(key: String, defaultValue: Int = 0) = settings.getInt(key, defaultValue) /** @@ -57,9 +55,7 @@ object StorageUtils { * @param [key] 键 * @param [value] 值 */ - fun saveLong(key: String, value: Long) { - settings.putLong(key,value) - } + fun saveLong(key: String, value: Long) = settings.putLong(key, value) /** * 获取长整数值 @@ -67,18 +63,14 @@ object StorageUtils { * @param defaultValue 默认值 * @return 存储的长整数值或默认值 */ - fun getLong(key: String, defaultValue: Long): Long { - return settings.getLong(key,defaultValue) - } + fun getLong(key: String, defaultValue: Long = 0L) = settings.getLong(key, defaultValue) /** * 保存布尔值 * @param key 键 * @param value 值 */ - fun saveBoolean(key: String, value: Boolean) { - settings.putBoolean(key,value) - } + fun saveBoolean(key: String, value: Boolean) = settings.putBoolean(key, value) /** * 获取布尔值 @@ -86,18 +78,16 @@ object StorageUtils { * @param defaultValue 默认值 * @return 存储的布尔值或默认值 */ - fun getBoolean(key: String, defaultValue: Boolean = false): Boolean { - return settings.getBoolean(key,defaultValue) - } + fun getBoolean(key: String, defaultValue: Boolean = false) = + settings.getBoolean(key, defaultValue) /** * 保存对象 * @param key 键 * @param value 对象 */ - inline fun saveObject(key: String, value: T) { - settings[key] = value - } + inline fun saveObject(key: String, value: T) = + settings.putString(key, json.encodeToString(value)) /** * 获取对象 @@ -105,7 +95,8 @@ object StorageUtils { * @return 存储的对象或null */ inline fun getObject(key: String): T? { - return settings[key] + val stored = settings.getStringOrNull(key) ?: return null + return runCatching { json.decodeFromString(stored) }.getOrNull() } /** diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt index 22729ca..636b191 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt @@ -17,4 +17,11 @@ object Constant { const val WEB_CLIENT_ID = "649192447921-rtn0jklurc7cr4oalh9gh3684mnlklce.apps.googleusercontent.com" + + /** 密钥令牌信息 */ + const val KEY_TOKEN_INFO = "tokenInfo" + + /** 关键用户信息 */ + const val KEY_USER_INFO = "userInfo" + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt index b6494c6..91b9a56 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt @@ -63,6 +63,17 @@ class CountdownRepositoryImpl( countdownDao.deleteCountdown(id) } + override suspend fun clearExpiredCountdowns() { + countdownDao.clearExpiredCountdowns() + } + + /** + * 删除所有倒计时 + */ + override suspend fun deleteAllCountdowns() { + countdownDao.deleteAllCountdowns() + } + /** * 获取倒数日通过类别 * @param [category] 类别 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt index e8f53ef..53c1ade 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt @@ -63,6 +63,20 @@ class TaskRepositoryImpl( taskDao.deleteTask(id) } + /** + * 删除已完成任务 + */ + override suspend fun deleteCompletedTasks() { + taskDao.deleteCompletedTasks() + } + + /** + * 删除所有任务 + */ + override suspend fun deleteAllTasks() { + taskDao.deleteAllTasks() + } + /** * 按类别获取任务 * @param [categoryId] 类别 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt index 5d99235..c24c929 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt @@ -81,13 +81,13 @@ interface CategoryDao { /** * 更新任务计数 */ - @Query("UPDATE categories SET taskCount = (SELECT COUNT(*) FROM tasks WHERE category = categories.name) WHERE type = 'TASK'") + @Query("UPDATE categories SET taskCount = (SELECT COUNT(*) FROM tasks WHERE category = categories.id) WHERE type = 'TASK'") suspend fun updateTaskCounts() /** * 更新倒计时计数 */ - @Query("UPDATE categories SET countdownCount = (SELECT COUNT(*) FROM countdowns WHERE category = categories.name) WHERE type = 'COUNTDOWN'") + @Query("UPDATE categories SET countdownCount = (SELECT COUNT(*) FROM countdowns WHERE category = categories.id) WHERE type = 'COUNTDOWN'") suspend fun updateCountdownCounts() /** @@ -100,10 +100,10 @@ interface CategoryDao { COALESCE(t.task_count, 0) as taskCount, COALESCE(cd.countdown_count, 0) as countdownCount FROM categories c - LEFT JOIN (SELECT category, COUNT(*) as task_count FROM tasks GROUP BY category) t ON c.name = t.category - LEFT JOIN (SELECT category, COUNT(*) as countdown_count FROM countdowns GROUP BY category) cd ON c.name = cd.category + LEFT JOIN (SELECT category, COUNT(*) as task_count FROM tasks GROUP BY category) t ON c.id = t.category + LEFT JOIN (SELECT category, COUNT(*) as countdown_count FROM countdowns GROUP BY category) cd ON c.id = cd.category WHERE c.type = :type ORDER BY c.createdAt ASC """) fun getCategoriesWithCounts(type: String): Flow> -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt index 53b0460..acc21bc 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt @@ -60,6 +60,13 @@ interface CountdownDao { @Query("DELETE FROM countdowns WHERE id = :id") suspend fun deleteCountdown(id: String) + + /** + * 清除过期倒计时 + */ + @Query("DELETE FROM countdowns WHERE isActive = false") + suspend fun clearExpiredCountdowns() + /** * 删除所有倒数日 */ diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt index a27552e..46174c5 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt @@ -74,6 +74,12 @@ interface TaskDao { @Query("DELETE FROM tasks WHERE id = :id") suspend fun deleteTask(id: String) + /** + * 删除已完成任务 + */ + @Query("DELETE FROM tasks WHERE isCompleted = true") + suspend fun deleteCompletedTasks() + /** * 删除所有任务 */ diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt index 6eadde8..da46f54 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt @@ -3,15 +3,21 @@ package com.taskttl.data.source.remote.api import com.taskttl.core.domain.constant.PointEvent import com.taskttl.core.domain.toJson import com.taskttl.core.network.ApiConfig +import com.taskttl.core.network.AuthHttpClient import com.taskttl.core.network.KtorClient import com.taskttl.core.utils.DeviceUtils -import com.taskttl.core.utils.LogUtils +import com.taskttl.data.source.remote.api.TaskTTLApi.refreshToken import com.taskttl.data.source.remote.dto.request.FeedbackReq -import com.taskttl.data.source.remote.dto.request.LoginReq import com.taskttl.data.source.remote.dto.request.PointReq +import com.taskttl.data.source.remote.dto.request.RefreshTokenRequest +import com.taskttl.data.source.remote.dto.request.ThirdPartyLoginReq import com.taskttl.data.source.remote.dto.response.Account -import com.taskttl.data.source.remote.dto.response.AuthResult import com.taskttl.data.source.remote.dto.response.FeedbackResp +import com.taskttl.data.source.remote.dto.response.ThirdPartyLoginResp +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType import org.jetbrains.compose.resources.getString import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.feedback_error @@ -21,12 +27,31 @@ object TaskTTLApi { /** * 第三方登录 * @param [account] 账户 - * @return [FeedbackResp] + * @return [ThirdPartyLoginResp] */ - suspend fun thirdPartyLogin(account: Account): FeedbackResp { + suspend fun thirdPartyLogin(account: Account): ThirdPartyLoginResp { try { - val loginReq = LoginReq(account.id,account.idToken,account.provider.name) - return KtorClient.postJson(ApiConfig.THIRD_PARTY_LOGIN_URL,loginReq.toJson()) + val thirdPartyLoginReq = ThirdPartyLoginReq.fromAccount(account) + thirdPartyLoginReq.baseReq = DeviceUtils.getDeviceInfo() + return KtorClient.postJson(ApiConfig.THIRD_PARTY_LOGIN_URL, thirdPartyLoginReq.toJson()) + } catch (_: Exception) { + throw Exception(getString(Res.string.feedback_error)) + } + } + + /** + * 刷新令牌 + * @param [refreshToken] 刷新令牌 + * @return [ThirdPartyLoginResp] + */ + suspend fun refreshToken(refreshToken: RefreshTokenRequest): ThirdPartyLoginResp { + try { + refreshToken.baseReq = DeviceUtils.getDeviceInfo() + val response = AuthHttpClient.client.post(ApiConfig.REFRESH_TOKEN_URL) { + contentType(ContentType.Application.Json) + setBody(refreshToken.toJson()) + } + return KtorClient.handleResponse(response) } catch (_: Exception) { throw Exception(getString(Res.string.feedback_error)) } @@ -41,7 +66,7 @@ object TaskTTLApi { try { feedbackReq.baseReq = DeviceUtils.getDeviceInfo() return KtorClient.postJson(ApiConfig.FEEDBACK_URL, feedbackReq.toJson()) - } catch (e: Exception) { + } catch (_: Exception) { throw Exception(getString(Res.string.feedback_error)) } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt deleted file mode 100644 index 9eb19ec..0000000 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.taskttl.data.source.remote.dto.request - -import com.taskttl.core.domain.BaseReqWith -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * 登录请求 - * @author DevTTL - * @date 2025/11/13 - * @constructor 创建[LoginReq] - * @param [id] id - * @param [idToken] id令牌 - * @param [provider] 提供者 - */ -@Serializable -data class LoginReq( - @SerialName("id") - val id: String, - @SerialName("idToken") - val idToken: String, - @SerialName("provider") - val provider: String, -) : BaseReqWith() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt new file mode 100644 index 0000000..13a2a98 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt @@ -0,0 +1,16 @@ +package com.taskttl.data.source.remote.dto.request + +import com.taskttl.core.domain.BaseReqWith +import kotlinx.serialization.Serializable + +/** + * 刷新令牌请求 + * @author admin + * @date 2025/11/26 + * @constructor 创建[RefreshTokenRequest] + * @param [refreshToken] 刷新令牌 + */ +@Serializable +class RefreshTokenRequest( + val refreshToken: String +) : BaseReqWith() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt new file mode 100644 index 0000000..13a77f8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt @@ -0,0 +1,39 @@ +package com.taskttl.data.source.remote.dto.request + +import com.taskttl.core.domain.BaseReqWith +import com.taskttl.data.source.remote.dto.response.Account +import kotlinx.serialization.Serializable + +/** + * 第三聚会登录请求 + * @author admin + * @date 2025/11/23 + * @constructor 创建[ThirdPartyLoginReq] + * @param [provider] 提供者 + * @param [idToken] ID令牌 + * @param [baseReq] 基础请求 + */ +@Serializable +class ThirdPartyLoginReq( + val id: String, + val provider: String, + val idToken: String, +) : BaseReqWith() { + + companion object { + + /** + * 获取通过账户 + * @param [account] 账户 + * @return [ThirdPartyLoginReq] + */ + fun fromAccount(account: Account): ThirdPartyLoginReq { + return ThirdPartyLoginReq( + id = account.id, + provider = account.provider.name, + idToken = account.idToken + ) + } + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt index c20f8aa..f9351ae 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt @@ -2,6 +2,12 @@ package com.taskttl.data.source.remote.dto.response import com.taskttl.data.constant.ProviderEnum +/** + * 身份验证结果 + * @author admin + * @date 2025/11/25 + * @constructor 创建[AuthResult] + */ sealed class AuthResult { data class Success(val account: Account) : AuthResult() data class Error(val message: String?) : AuthResult() diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt new file mode 100644 index 0000000..bc58cf1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt @@ -0,0 +1,32 @@ +package com.taskttl.data.source.remote.dto.response + +import com.taskttl.core.auth.TokenInfo +import kotlinx.serialization.Serializable + +/** + * 刷新令牌响应 + * @author admin + * @date 2025/11/26 + * @constructor 创建[RefreshTokenResponse] + * @param [accessToken] 访问令牌 + * @param [refreshToken] 刷新令牌 + * @param [expiresIn] 到期时间剩余 + */ +@Serializable +data class RefreshTokenResponse( + val accessToken: String, // 访问令牌 + val refreshToken: String, // 刷新令牌 + val expiresIn: Long // 到期时间剩余 +) { + /** + * 转成令牌信息 + * @return [TokenInfo] + */ + fun toTokenInfo(): TokenInfo { + return TokenInfo( + accessToken = accessToken, + refreshToken = refreshToken, + expiresAt = expiresIn + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt new file mode 100644 index 0000000..9f69c06 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt @@ -0,0 +1,55 @@ +package com.taskttl.data.source.remote.dto.response + +import com.taskttl.core.auth.TokenInfo +import com.taskttl.domain.model.UserInfo +import kotlinx.serialization.Serializable + +/** + * 第三方登录响应 + * @author admin + * @date 2025/11/25 + * @constructor 创建[ThirdPartyLoginResp] + * @param [userId] 用户ID + * @param [nickname] 昵称 + * @param [avatar] 头像 + * @param [email] 电子邮件 + * @param [accessToken] 访问令牌 + * @param [refreshToken] 刷新令牌 + * @param [expiresAt] 到期日 + */ +@Serializable +data class ThirdPartyLoginResp( + var userId: Long, // 用户ID + var nickname: String, // 昵称 + var avatar: String, // 头像 + var email: String, // 电子邮件 + var accessToken: String, // 访问令牌 + var refreshToken: String, // 刷新令牌 + var expiresAt: Long, // 到期日 +) { + /** + * 转成令牌信息 + * @return [TokenInfo] + */ + fun toTokenInfo(): TokenInfo { + return TokenInfo( + accessToken = accessToken, + refreshToken = refreshToken, + expiresAt = expiresAt + ) + } + + /** + * 转成用户信息 + * @return [UserInfo] + */ + fun toUserInfo(): UserInfo { + return UserInfo( + userId = userId, + nickname = nickname, + avatar = avatar, + email = email + ) + } +} + diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt new file mode 100644 index 0000000..29dc3e8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt @@ -0,0 +1,21 @@ +package com.taskttl.domain.model + +import kotlinx.serialization.Serializable + +/** + * 用户信息 + * @author admin + * @date 2025/11/23 + * @constructor 创建[UserInfo] + * @param [userId] 用户ID + * @param [nickname] 昵称 + * @param [avatar] 头像 + * @param [email] 电子邮件 + */ +@Serializable +data class UserInfo( + var userId: Long, // 用户ID + var nickname: String, // 昵称 + var avatar: String, // 头像 + var email: String, // 电子邮件 +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt index f4b6edb..c9ffdb7 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt @@ -41,6 +41,16 @@ interface CountdownRepository { */ suspend fun deleteCountdown(id: String) + /** + * 清除过期倒计时 + */ + suspend fun clearExpiredCountdowns() + + /** + * 删除所有倒计时 + */ + suspend fun deleteAllCountdowns() + /** * 获取倒数日通过类别 * @param [category] 类别 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt index 04e13c9..ba5d14a 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt @@ -41,6 +41,16 @@ interface TaskRepository { */ suspend fun deleteTask(id: String) + /** + * 删除已完成任务 + */ + suspend fun deleteCompletedTasks() + + /** + * 删除所有任务 + */ + suspend fun deleteAllTasks() + /** * 按类别获取任务 * @param [category] 类别 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt index 30e4190..4877dc4 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt @@ -1,5 +1,7 @@ package com.taskttl.navigation // 主导航 +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -23,7 +25,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import com.taskttl.presentation.common.components.CustomBottomBar -import com.taskttl.presentation.features.auth.LoginScreen import com.taskttl.presentation.features.category.editor.CategoryEditScreen import com.taskttl.presentation.features.category.list.CategoryScreen import com.taskttl.presentation.features.countdown.detail.CountdownDetailScreen @@ -38,6 +39,8 @@ import com.taskttl.presentation.features.statistics.StatisticsScreen import com.taskttl.presentation.features.task.detail.TaskEditorScreen import com.taskttl.presentation.features.task.editor.TaskDetailScreen import com.taskttl.presentation.features.task.list.TaskScreen +import com.taskttl.presentation.features.user.login.LoginScreen +import com.taskttl.presentation.features.user.userProfile.UserProfileScreen import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.nav_countdown import taskttl.composeapp.generated.resources.nav_settings @@ -85,6 +88,34 @@ fun MainNav() { NavHost( modifier = Modifier.fillMaxSize() .padding(bottom = paddingValues.calculateBottomPadding()), + // 页面进入动画 + enterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(300) + ) + }, + // 页面退出动画 + exitTransition = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(300) + ) + }, + // 返回时页面进入动画 + popEnterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(300) + ) + }, + // 返回时页面退出动画 + popExitTransition = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(300) + ) + }, navController = mainNavController, startDestination = Routes.Main.Task ) { @@ -109,7 +140,13 @@ fun MainNav() { TaskDetailScreen( taskId = taskDetail.taskId, onNavigateBack = { mainNavController.popBackStack() }, - onNavigateToEdit = { mainNavController.navigate(Routes.Main.Task.EditTask(taskDetail.taskId)) } + onNavigateToEdit = { + mainNavController.navigate( + Routes.Main.Task.EditTask( + taskDetail.taskId + ) + ) + } ) } @@ -185,11 +222,16 @@ fun MainNav() { ) } - composable { + composable { LoginScreen( onNavigateBack = { mainNavController.popBackStack() } ) } + composable { + UserProfileScreen( + onNavigateBack = { mainNavController.popBackStack() } + ) + } } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt index 784d6df..fdf1839 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt @@ -174,13 +174,31 @@ sealed interface Routes { @Serializable data object About : Routes - /** - * 登录 - * @author DevTTL - * @date 2025/11/07 - */ - @Serializable - data object Login : Routes + } } + + /** + * 用户 + * @author DevTTL + * @date 2025/12/03 + */ + @Serializable + data object User : Routes { + /** + * 登录 + * @author DevTTL + * @date 2025/11/07 + */ + @Serializable + data object Login : Routes + + /** + * 用户档案 + * @author DevTTL + * @date 2025/12/03 + */ + @Serializable + data object UserProfile: Routes + } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt index ee7eaf0..84aab34 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt @@ -4,7 +4,6 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures @@ -36,6 +35,7 @@ import androidx.compose.ui.UiComposable import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @@ -46,7 +46,6 @@ import kotlin.math.sign private val PaddingSm = 8.dp -@OptIn(ExperimentalFoundationApi::class) @Composable fun ActionButtonListItem( modifier: Modifier = Modifier, @@ -282,6 +281,7 @@ private fun applyDamping( } } +@Preview @Composable private fun ActionButtonListItemPreview() { var isOpen by remember { mutableStateOf(false) } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt index b20c6be..4494106 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt @@ -7,6 +7,7 @@ import coil3.compose.AsyncImage import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.request.crossfade +import com.taskttl.core.utils.LogUtils import org.jetbrains.compose.resources.painterResource import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.ic_launcher @@ -23,6 +24,14 @@ fun NetworkImage( modifier = modifier, model = ImageRequest.Builder(context) .data(url) + .listener( + onError = { _, result -> + LogUtils.e("DevTTL_CoilError", "Load fail: ${result.throwable}") + }, + onSuccess = { _, _ -> + LogUtils.d("DevTTL_Coil", "Load success") + } + ) // 缓存策略:启用内存 + 磁盘缓存 .memoryCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED) diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt index ecf5c07..c0d2e06 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt @@ -7,7 +7,9 @@ import coil3.SingletonImageLoader import coil3.compose.LocalPlatformContext import coil3.disk.DiskCache import coil3.memory.MemoryCache +import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.request.crossfade +import com.taskttl.core.network.KtorClient import okio.Path /** @@ -19,6 +21,7 @@ fun GlobalImageLoader() { remember(platformContext) { val imageLoader = ImageLoader.Builder(platformContext) + .components { add(KtorNetworkFetcherFactory(KtorClient.httpClient)) } // 内存缓存 .memoryCache { MemoryCache.Builder() diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt deleted file mode 100644 index 5022eaa..0000000 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.taskttl.ui.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color - -/** 浅色方案 - 优化后的配色方案(Material Design 3风格)*/ -private val LightColorScheme = lightColorScheme( - // 主色调 - 更鲜艳的紫蓝色系,提升视觉冲击力 - primary = Color(0xFF5B6FF8), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFE1E4FF), - onPrimaryContainer = Color(0xFF001A41), - - // 次要色 - 优化紫色系,更柔和协调 - secondary = Color(0xFF8B5FBF), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFF3E8FF), - onSecondaryContainer = Color(0xFF2C0051), - - // 第三色 - 温暖的橙色系(用于倒计时等) - tertiary = Color(0xFFFF9500), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFFFE6CC), - onTertiaryContainer = Color(0xFF331F00), - - // 错误色 - 更现代的红色系 - error = Color(0xFFDC3545), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFE5E5), - onErrorContainer = Color(0xFF5F0010), - - // 背景色 - 更柔和的浅色背景 - background = Color(0xFFFAFBFC), - onBackground = Color(0xFF1A1C1E), - - // 表面色 - 纯净白色 - surface = Color(0xFFFFFFFF), - onSurface = Color(0xFF1A1C1E), - surfaceVariant = Color(0xFFE4E6EB), - onSurfaceVariant = Color(0xFF45464F), - - // 轮廓色 - 更细腻的边框 - outline = Color(0xFFCED0D6), - outlineVariant = Color(0xFFE4E6EB), - - // 其他 - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF2F3033), - inverseOnSurface = Color(0xFFF1F0F4), - inversePrimary = Color(0xFFAAB4FF), - - // 表面容器色 - 更丰富的层次 - surfaceDim = Color(0xFFDBDCE0), - surfaceBright = Color(0xFFFFFFFF), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFF5F6FA), - surfaceContainer = Color(0xFFEFF0F5), - surfaceContainerHigh = Color(0xFFE9EAF0), - surfaceContainerHighest = Color(0xFFE3E4EA) -) - -/** 深色配色方案 - 优化后的深色模式(Material Design 3风格)*/ -private val DarkColorScheme = darkColorScheme( - // 主色调 - 柔和亮丽的紫蓝色系 - primary = Color(0xFFAAB4FF), - onPrimary = Color(0xFF001A41), - primaryContainer = Color(0xFF3D4DB8), - onPrimaryContainer = Color(0xFFE1E4FF), - - // 次要色 - 优雅的亮紫色系 - secondary = Color(0xFFD4BCFF), - onSecondary = Color(0xFF2C0051), - secondaryContainer = Color(0xFF5A3D85), - onSecondaryContainer = Color(0xFFF3E8FF), - - // 第三色 - 温暖的亮橙色系 - tertiary = Color(0xFFFFBB5C), - onTertiary = Color(0xFF331F00), - tertiaryContainer = Color(0xFFCC7700), - onTertiaryContainer = Color(0xFFFFE6CC), - - // 错误色 - 柔和的亮红色系 - error = Color(0xFFFF7782), - onError = Color(0xFF5F0010), - errorContainer = Color(0xFFB42734), - onErrorContainer = Color(0xFFFFE5E5), - - // 背景色 - OLED友好的纯黑 - background = Color(0xFF1A1C1E), - onBackground = Color(0xFFE3E2E6), - - // 表面色 - 稍亮的深色 - surface = Color(0xFF1A1C1E), - onSurface = Color(0xFFE3E2E6), - surfaceVariant = Color(0xFF45464F), - onSurfaceVariant = Color(0xFFC5C6D0), - - // 轮廓色 - 更好的边框可见度 - outline = Color(0xFF8F9099), - outlineVariant = Color(0xFF45464F), - - // 其他 - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFE3E2E6), - inverseOnSurface = Color(0xFF2F3033), - inversePrimary = Color(0xFF5B6FF8), - - // 表面容器色 - 深色模式下的精细层次 - surfaceDim = Color(0xFF121316), - surfaceBright = Color(0xFF393B3F), - surfaceContainerLowest = Color(0xFF0D0E11), - surfaceContainerLow = Color(0xFF1A1C1E), - surfaceContainer = Color(0xFF1E2023), - surfaceContainerHigh = Color(0xFF282A2D), - surfaceContainerHighest = Color(0xFF333538) -) - -/** - * 应用主题 - * @param [darkTheme] 黑暗主题 - * @param [content] 内容 - */ -@Composable -fun AppTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { - val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme - - MaterialTheme( - colorScheme = colorScheme, - typography = mainTypography(), - content = content - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt deleted file mode 100644 index 1ddadb0..0000000 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.taskttl.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.runtime.Composable -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - - -/** - * 主要排版 - * @return [Typography] - */ -@Composable -fun mainTypography(): Typography { - return Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt deleted file mode 100644 index 23b487b..0000000 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.taskttl.presentation.features.auth - -import androidx.compose.runtime.Stable -import androidx.lifecycle.viewModelScope -import com.taskttl.core.base.BaseViewModel -import com.taskttl.core.utils.LogUtils -import com.taskttl.data.source.remote.api.TaskTTLApi -import com.taskttl.data.source.remote.dto.response.AuthResult -import com.taskttl.domain.repository.AuthRepository -import kotlinx.coroutines.launch - -/** - * 身份验证视图模型 - * @author admin - * @date 2025/10/26 - * @constructor 创建[AuthViewModel] - */ -@Stable -class AuthViewModel(private val authRepository: AuthRepository) : - BaseViewModel(AuthState()) { - - - public override fun handleIntent(intent: AuthIntent) { - when (intent) { - is AuthIntent.LoginWithGoogle -> onLoginWithGoogle() - - is AuthIntent.LoginWithFacebook -> onLoginWithFacebook() - - is AuthIntent.ClearError -> clearError() - is AuthIntent.Logout -> {} - } - } - - private fun setLoading(loading: Boolean) { - updateState { copy(isLoading = loading, isProcessing = false, error = null) } - } - - /** - * 清除错误 - */ - private fun clearError() { - updateState { copy(error = null) } - } - - /** - * 使用谷歌登录 - */ - private fun onLoginWithGoogle() { - LogUtils.e("DevTTL", "Google 登录触发") - viewModelScope.launch { - when (val result = authRepository.loginWithGoogle()) { - is AuthResult.Success -> { - if (result.account.isEmpty) { - updateState { copy(isLoading = false, error = "Login canceled") } - return@launch - } - - TaskTTLApi.thirdPartyLogin(result.account) - updateState { copy(isLoading = false, error = null) } - // sendEvent(AuthEffect.NavigateToHome) - } - - is AuthResult.Canceled -> { - updateState { copy(isLoading = false, error = "Login canceled") } - sendEvent(AuthEffect.ShowMessage("Login canceled")) - } - - is AuthResult.Error -> { - updateState { - copy( - isLoading = false, - error = result.message ?: "Unknown error" - ) - } - sendEvent(AuthEffect.ShowMessage(result.message ?: "Unknown error")) - } - } - } - } - - /** - * 使用脸书登录 - */ - private fun onLoginWithFacebook() { - LogUtils.e("DevTTL", "Facebook 登录触发") - viewModelScope.launch { - val result = authRepository.loginWithFacebook() - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt index 9fec21a..acacad7 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import com.taskttl.core.ui.component.empty.Empty import com.taskttl.core.utils.ToastUtils import com.taskttl.domain.model.Category @@ -70,6 +71,8 @@ import taskttl.composeapp.generated.resources.label_countdown_count import taskttl.composeapp.generated.resources.edit import taskttl.composeapp.generated.resources.label_no_category import taskttl.composeapp.generated.resources.label_task_count +import taskttl.composeapp.generated.resources.text_add_task_hint +import taskttl.composeapp.generated.resources.text_no_tasks import taskttl.composeapp.generated.resources.title_add_category import taskttl.composeapp.generated.resources.title_category @@ -93,8 +96,6 @@ fun CategoryScreen( is CategoryEffect.NavigateBack -> { onNavigateBack.invoke() } - - else -> {} } } } @@ -150,23 +151,10 @@ fun CategoryScreen( CircularProgressIndicator() } } else if (categories.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(Res.string.label_no_category), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(Res.string.label_add_category_hint), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + Empty( + message = Res.string.label_no_category, + subtitle = Res.string.label_add_category_hint + ) } else { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp) diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt index 20ec3ad..2183fc7 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt @@ -87,8 +87,19 @@ sealed class CategoryIntent { * @constructor 创建[CategoryEffect] */ sealed class CategoryEffect { - data class ShowMessage(val message: String) : CategoryEffect() - data class NavigateToCategoryDetail(val categoryId: String) : CategoryEffect() + /** + * 导航返回 + * @author admin + * @date 2025/11/26 + */ object NavigateBack : CategoryEffect() - data class ShowConfirmDialog(val message: String, val onConfirm: () -> Unit) : CategoryEffect() + + /** + * 显示消息 + * @author admin + * @date 2025/11/26 + * @constructor 创建[ShowMessage] + * @param [message] 消息 + */ + data class ShowMessage(val message: String) : CategoryEffect() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt index 2c1a5f8..5c5e518 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import com.taskttl.core.ui.component.empty.Empty import com.taskttl.core.utils.DateUtils import com.taskttl.core.utils.ToastUtils import com.taskttl.domain.model.Countdown @@ -150,23 +151,10 @@ fun CountdownScreen( } state.filteredCountdowns.isEmpty() -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(Res.string.text_no_countdowns), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(Res.string.text_add_countdown_tip), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + Empty( + message = Res.string.text_no_countdowns, + subtitle = Res.string.text_add_countdown_tip + ) } else -> { diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt index 89d0c64..f22583b 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt @@ -52,6 +52,20 @@ sealed class CountdownIntent { * @constructor 创建[CountdownEffect] */ sealed class CountdownEffect { - data class ShowMessage(val message: String) : CountdownEffect() + /** + * 导航返回 + * @author admin + * @date 2025/11/26 + */ object NavigateBack : CountdownEffect() + + /** + * 显示消息 + * @author admin + * @date 2025/11/26 + * @constructor 创建[ShowMessage] + * @param [message] 消息 + */ + data class ShowMessage(val message: String) : CountdownEffect() + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt index 5a00b2e..bb43703 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.onboarding - +// 引导视图 import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt index ae44f08..5cc6ada 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.onboarding - +// 引导状态 import com.taskttl.core.base.BaseState import com.taskttl.domain.model.OnboardingPage diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt index 1ca4a49..f82f963 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch /** - * 入职视图模型 + * 引导视图模型 * @author admin * @date 2025/10/05 * @constructor 创建[OnboardingViewModel] diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt index 39a3895..81e0fe5 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.settings.about - +// 关于屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.taskttl.core.ui.component.divider.Divider import com.taskttl.presentation.common.components.AppHeader import com.taskttl.presentation.features.settings.main.SettingsIntent import com.taskttl.presentation.features.settings.main.SettingsViewModel @@ -59,6 +60,11 @@ import taskttl.composeapp.generated.resources.version import taskttl.composeapp.generated.resources.web_text import taskttl.composeapp.generated.resources.web_url +/** + * 关于屏幕 + * @param [onNavigateBack] 上导航返回 + * @param [viewModel] 视图模型 + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun AboutScreen( @@ -131,6 +137,9 @@ fun AboutScreen( ) } } + + Divider() + Spacer(modifier = Modifier.height(16.dp)) // 应用描述 Card( diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt index e8839d5..54d067d 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt @@ -1,86 +1,78 @@ package com.taskttl.presentation.features.settings.dataManagement - +// 数据管理屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AttachFile -import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.CleaningServices -import androidx.compose.material.icons.filled.CloudDownload -import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.History -import androidx.compose.material.icons.filled.Sync -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.RadioButton import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import com.taskttl.core.utils.ToastUtils import com.taskttl.presentation.common.components.AppHeader -import org.jetbrains.compose.resources.StringResource +import com.taskttl.presentation.features.settings.dataManagement.components.ConfirmDialog +import com.taskttl.presentation.features.settings.dataManagement.components.DataManagementCard +import com.taskttl.presentation.features.settings.dataManagement.components.ExportDataDialog +import com.taskttl.presentation.features.settings.dataManagement.components.ImportDataDialog import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res -import taskttl.composeapp.generated.resources.cancel -import taskttl.composeapp.generated.resources.confirm -import taskttl.composeapp.generated.resources.desc_auto_backup import taskttl.composeapp.generated.resources.desc_clear_all_data import taskttl.composeapp.generated.resources.desc_clear_all_data_dialog import taskttl.composeapp.generated.resources.desc_clear_completed_tasks import taskttl.composeapp.generated.resources.desc_clear_expired_countdowns -import taskttl.composeapp.generated.resources.desc_export_data -import taskttl.composeapp.generated.resources.desc_import_data -import taskttl.composeapp.generated.resources.export -import taskttl.composeapp.generated.resources.import -import taskttl.composeapp.generated.resources.label_csv_format -import taskttl.composeapp.generated.resources.enter -import taskttl.composeapp.generated.resources.label_json_format -import taskttl.composeapp.generated.resources.label_select_export_format -import taskttl.composeapp.generated.resources.label_select_file -import taskttl.composeapp.generated.resources.label_select_import_file -import taskttl.composeapp.generated.resources.title_auto_backup -import taskttl.composeapp.generated.resources.title_backup_restore import taskttl.composeapp.generated.resources.title_clear_all_data import taskttl.composeapp.generated.resources.title_clear_completed_tasks import taskttl.composeapp.generated.resources.title_clear_expired_countdowns import taskttl.composeapp.generated.resources.title_data_clean import taskttl.composeapp.generated.resources.title_data_management -import taskttl.composeapp.generated.resources.title_export_data -import taskttl.composeapp.generated.resources.title_import_data +/** + * 数据管理屏幕 + * @param [onNavigateBack] 上导航返回 + * @param [viewModel] 视图模型 + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun DataManagementScreen( - onNavigateBack: () -> Unit + onNavigateBack: () -> Unit, + viewModel: DataManagementViewModel = koinViewModel() ) { + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + viewModel.effects.collect { effect -> + when (effect) { + is DataManagementEffect.NavigateBack -> onNavigateBack.invoke() + is DataManagementEffect.ShowMessage -> ToastUtils.show(effect.message) + } + } + } + var showExportDialog by remember { mutableStateOf(false) } var showImportDialog by remember { mutableStateOf(false) } + var showClearDataDialog by remember { mutableStateOf(false) } + var showClearCompletedTasksDialog by remember { mutableStateOf(false) } + var showClearExpiredCountdownsDialog by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -103,39 +95,39 @@ fun DataManagementScreen( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - item { - Text( - text = stringResource(Res.string.title_backup_restore), - style = MaterialTheme.typography.titleLarge - ) - } - - item { - DataManagementCard( - icon = Icons.Default.CloudUpload, - titleRes = Res.string.title_export_data, - descriptionRes = Res.string.desc_export_data, - onClick = { showExportDialog = true } - ) - } - - item { - DataManagementCard( - icon = Icons.Default.CloudDownload, - titleRes = Res.string.title_import_data, - descriptionRes = Res.string.desc_import_data, - onClick = { showImportDialog = true } - ) - } - - item { - DataManagementCard( - icon = Icons.Default.Sync, - titleRes = Res.string.title_auto_backup, - descriptionRes = Res.string.desc_auto_backup, - onClick = { /* TODO: 自动备份设置 */ } - ) - } + // item { + // Text( + // text = stringResource(Res.string.title_backup_restore), + // style = MaterialTheme.typography.titleLarge + // ) + // } + // + // item { + // DataManagementCard( + // icon = Icons.Default.CloudUpload, + // titleRes = Res.string.title_export_data, + // descriptionRes = Res.string.desc_export_data, + // onClick = { showExportDialog = true } + // ) + // } + // + // item { + // DataManagementCard( + // icon = Icons.Default.CloudDownload, + // titleRes = Res.string.title_import_data, + // descriptionRes = Res.string.desc_import_data, + // onClick = { showImportDialog = true } + // ) + // } + // + // item { + // DataManagementCard( + // icon = Icons.Default.Sync, + // titleRes = Res.string.title_auto_backup, + // descriptionRes = Res.string.desc_auto_backup, + // onClick = { /* TODO: 自动备份设置 */ } + // ) + // } item { Spacer(modifier = Modifier.height(16.dp)) @@ -145,6 +137,7 @@ fun DataManagementScreen( ) } + // 清除全部数据 item { DataManagementCard( icon = Icons.Default.Delete, @@ -155,21 +148,23 @@ fun DataManagementScreen( ) } + // 清理已完成任务 item { DataManagementCard( icon = Icons.Default.CleaningServices, titleRes = Res.string.title_clear_completed_tasks, descriptionRes = Res.string.desc_clear_completed_tasks, - onClick = { /* TODO: 清理已完成任务 */ } + onClick = { showClearCompletedTasksDialog = true } ) } + // 清理过期倒数日 item { DataManagementCard( icon = Icons.Default.History, titleRes = Res.string.title_clear_expired_countdowns, descriptionRes = Res.string.desc_clear_expired_countdowns, - onClick = { /* TODO: 清理过期倒数日 */ } + onClick = { showClearExpiredCountdownsDialog = true } ) } } @@ -198,173 +193,35 @@ fun DataManagementScreen( } // 清除数据确认对话框 - if (showClearDataDialog) { - AlertDialog( - onDismissRequest = { showClearDataDialog = false }, - title = { Text(stringResource(Res.string.title_clear_all_data)) }, - text = { Text(stringResource(Res.string.desc_clear_all_data_dialog)) }, - confirmButton = { - TextButton( - onClick = { - // TODO: 实现清除数据功能 - showClearDataDialog = false - } - ) { - Text(stringResource(Res.string.confirm), color = MaterialTheme.colorScheme.error) - } - }, - dismissButton = { - TextButton(onClick = { showClearDataDialog = false }) { - Text(stringResource(Res.string.cancel)) - } - } - ) - } + ConfirmDialog( + visible = showClearDataDialog, + title = stringResource(Res.string.title_clear_all_data), + message = stringResource(Res.string.desc_clear_all_data_dialog), + confirmIsDestructive = true, + onConfirm = { viewModel.processIntent(DataManagementIntent.ClearAllData) }, + onDismiss = { showClearDataDialog = false } + ) + + + // 清理已完成任务确认对话框 + ConfirmDialog( + visible = showClearCompletedTasksDialog, + title = stringResource(Res.string.title_clear_completed_tasks), + message = stringResource(Res.string.desc_clear_completed_tasks), + confirmIsDestructive = true, + onConfirm = { viewModel.processIntent(DataManagementIntent.ClearCompletedTasks) }, + onDismiss = { showClearCompletedTasksDialog = false } + ) + + + // 清理过期倒数日确认对话框 + ConfirmDialog( + visible = showClearExpiredCountdownsDialog, + title = stringResource(Res.string.title_clear_expired_countdowns), + message = stringResource(Res.string.desc_clear_expired_countdowns), + confirmIsDestructive = true, + onConfirm = { viewModel.processIntent(DataManagementIntent.ClearExpiredCountdowns) }, + onDismiss = { showClearExpiredCountdownsDialog = false } + ) } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun DataManagementCard( - icon: ImageVector, - titleRes: StringResource, - descriptionRes: StringResource, - onClick: () -> Unit, - isDestructive: Boolean = false -) { - Card( - onClick = onClick, - colors = if (isDestructive) { - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) - ) - } else { - CardDefaults.cardColors() - } - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = stringResource(titleRes), - tint = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp) - ) - - Spacer(modifier = Modifier.width(16.dp)) - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(titleRes), - style = MaterialTheme.typography.titleMedium, - color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(descriptionRes), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Icon( - imageVector = Icons.Default.ChevronRight, - contentDescription = stringResource(Res.string.enter), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - -@Composable -private fun ExportDataDialog( - onDismiss: () -> Unit, - onExport: (String) -> Unit -) { - var selectedFormat by remember { mutableStateOf("JSON") } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(Res.string.title_export_data)) }, - text = { - Column { - Text("${stringResource(Res.string.label_select_export_format)}:") - Spacer(modifier = Modifier.height(16.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedFormat == "JSON", - onClick = { selectedFormat = "JSON" } - ) - Text(stringResource(Res.string.label_json_format)) - } - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedFormat == "CSV", - onClick = { selectedFormat = "CSV" } - ) - Text(stringResource(Res.string.label_csv_format)) - } - } - }, - confirmButton = { - TextButton(onClick = { onExport(selectedFormat) }) { - Text(stringResource(Res.string.export)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(Res.string.cancel)) - } - } - ) -} - -@Composable -private fun ImportDataDialog( - onDismiss: () -> Unit, - onImport: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(Res.string.title_import_data)) }, - text = { - Column { - Text("${stringResource(Res.string.label_select_import_file)}:") - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedButton( - onClick = { /* TODO: 文件选择器 */ }, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - Icons.Default.AttachFile, - contentDescription = stringResource(Res.string.label_select_file) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(Res.string.label_select_file)) - } - } - }, - confirmButton = { - TextButton(onClick = onImport) { - Text(stringResource(Res.string.import)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(Res.string.cancel)) - } - } - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt new file mode 100644 index 0000000..72f39c2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt @@ -0,0 +1,61 @@ +package com.taskttl.presentation.features.settings.dataManagement +// 数据管理状态 +import com.taskttl.core.base.BaseState + +/** + * 数据管理状态 + * @author admin + * @date 2025/11/26 + * @constructor 创建[DataManagementState] + * @param [isLoading] 正在加载 + * @param [isProcessing] 正在处理 + * @param [error] 错误 + */ +data class DataManagementState( + override val isLoading: Boolean = false, + override val isProcessing: Boolean = false, + override val error: String? = null, +) : BaseState() + +/** + * 数据管理意图 + * @author admin + * @date 2025/11/26 + * @constructor 创建[DataManagementIntent] + */ +sealed class DataManagementIntent { + /** 清除所有数据 */ + data object ClearAllData : DataManagementIntent() + + /** 清理已完成任务 */ + data object ClearCompletedTasks : DataManagementIntent() + + /** 清理过期倒数日 */ + data object ClearExpiredCountdowns : DataManagementIntent() +} + + +/** + * 数据管理效果 + * @author admin + * @date 2025/11/26 + * @constructor 创建[DataManagementEffect] + */ +sealed class DataManagementEffect { + + /** + * 导航返回 + * @author admin + * @date 2025/10/12 + */ + object NavigateBack : DataManagementEffect() + + /** + * 显示消息 + * @author admin + * @date 2025/10/12 + * @constructor 创建[ShowMessage] + * @param [message] 消息 + */ + data class ShowMessage(val message: String) : DataManagementEffect() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt new file mode 100644 index 0000000..1202c4c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt @@ -0,0 +1,62 @@ +package com.taskttl.presentation.features.settings.dataManagement + +import androidx.compose.runtime.Stable +import androidx.lifecycle.viewModelScope +import com.taskttl.core.base.BaseViewModel +import com.taskttl.domain.repository.CountdownRepository +import com.taskttl.domain.repository.TaskRepository +import kotlinx.coroutines.launch + +/** + * 数据管理视图模型 + * @author admin + * @date 2025/11/26 + * @constructor 创建[DataManagementViewModel] + */ +@Stable +class DataManagementViewModel( + private val taskRepository: TaskRepository, + private val countdownRepository: CountdownRepository +) : BaseViewModel(DataManagementState()) { + + + override fun handleIntent(intent: DataManagementIntent) { + when (intent) { + is DataManagementIntent.ClearAllData -> clearAllData() + is DataManagementIntent.ClearCompletedTasks -> clearCompletedTasks() + is DataManagementIntent.ClearExpiredCountdowns -> clearExpiredCountdowns() + } + } + + /** + * 清除所有数据 + */ + private fun clearAllData() { + viewModelScope.launch { + taskRepository.deleteAllTasks() + countdownRepository.deleteAllCountdowns() + sendEvent(DataManagementEffect.ShowMessage("已清除所有数据")) + } + } + + /** + * 清除已完成任务 + */ + private fun clearCompletedTasks() { + viewModelScope.launch { + taskRepository.deleteCompletedTasks() + sendEvent(DataManagementEffect.ShowMessage("已清除已完成任务")) + } + } + + /** + * 清除过期倒计时 + */ + private fun clearExpiredCountdowns() { + viewModelScope.launch { + countdownRepository.clearExpiredCountdowns() + sendEvent(DataManagementEffect.ShowMessage("已清除过期倒计时")) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt new file mode 100644 index 0000000..2c130b9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt @@ -0,0 +1,53 @@ +package com.taskttl.presentation.features.settings.dataManagement.components +// 确认对话框 +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import org.jetbrains.compose.resources.stringResource +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.cancel +import taskttl.composeapp.generated.resources.confirm + +/** + * 确认对话框 + * @param [visible] 可见 + * @param [title] 标题 + * @param [message] 消息 + * @param [confirmText] 确认文本 + * @param [cancelText] 取消文本 + * @param [confirmIsDestructive] 确认具有破坏性 + * @param [onConfirm] 确认 + * @param [onDismiss] 解雇 + */ +@Composable +fun ConfirmDialog( + visible: Boolean, + title: String, + message: String, + confirmText: String = stringResource(Res.string.confirm), + cancelText: String = stringResource(Res.string.cancel), + confirmIsDestructive: Boolean = false, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + if (!visible) return + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + TextButton(onClick = { + onConfirm() + onDismiss() + }) { + Text( + confirmText, + color = if (confirmIsDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary + ) + } + }, + dismissButton = { TextButton(onClick = onDismiss) { Text(cancelText) } } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt new file mode 100644 index 0000000..a4e1980 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt @@ -0,0 +1,92 @@ +package com.taskttl.presentation.features.settings.dataManagement.components +// 数据管理卡片 +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.enter + +/** + * 数据管理卡 + * @param [icon] 图标 + * @param [titleRes] 标题res + * @param [descriptionRes] 描述res + * @param [onClick] 点击时 + * @param [isDestructive] 具有破坏性 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DataManagementCard( + icon: ImageVector, + titleRes: StringResource, + descriptionRes: StringResource, + onClick: () -> Unit, + isDestructive: Boolean = false +) { + Card( + onClick = onClick, + colors = if (isDestructive) { + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) + ) + } else { + CardDefaults.cardColors() + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = stringResource(titleRes), + tint = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.titleMedium, + color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(descriptionRes), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = stringResource(Res.string.enter), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt new file mode 100644 index 0000000..716cc47 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt @@ -0,0 +1,81 @@ +package com.taskttl.presentation.features.settings.dataManagement.components +// 导出数据对话框 +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.cancel +import taskttl.composeapp.generated.resources.export +import taskttl.composeapp.generated.resources.label_csv_format +import taskttl.composeapp.generated.resources.label_json_format +import taskttl.composeapp.generated.resources.label_select_export_format +import taskttl.composeapp.generated.resources.title_export_data + + +/** + * 导出数据对话框 + * @param [onDismiss] 解雇 + * @param [onExport] 关于出口 + */ +@Composable +fun ExportDataDialog( + onDismiss: () -> Unit, + onExport: (String) -> Unit +) { + var selectedFormat by remember { mutableStateOf("JSON") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(Res.string.title_export_data)) }, + text = { + Column { + Text("${stringResource(Res.string.label_select_export_format)}:") + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedFormat == "JSON", + onClick = { selectedFormat = "JSON" } + ) + Text(stringResource(Res.string.label_json_format)) + } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedFormat == "CSV", + onClick = { selectedFormat = "CSV" } + ) + Text(stringResource(Res.string.label_csv_format)) + } + } + }, + confirmButton = { + TextButton(onClick = { onExport(selectedFormat) }) { + Text(stringResource(Res.string.export)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(Res.string.cancel)) + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt new file mode 100644 index 0000000..c83a97e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt @@ -0,0 +1,68 @@ +package com.taskttl.presentation.features.settings.dataManagement.components +// 导入数据对话框 +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.cancel +import taskttl.composeapp.generated.resources.import +import taskttl.composeapp.generated.resources.label_select_file +import taskttl.composeapp.generated.resources.label_select_import_file +import taskttl.composeapp.generated.resources.title_import_data + +/** + * 导入数据对话框 + * @param [onDismiss] 解雇 + * @param [onImport] 关于导入 + */ +@Composable +fun ImportDataDialog( + onDismiss: () -> Unit, + onImport: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(Res.string.title_import_data)) }, + text = { + Column { + Text("${stringResource(Res.string.label_select_import_file)}:") + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedButton( + onClick = { /* TODO: 文件选择器 */ }, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + Icons.Default.AttachFile, + contentDescription = stringResource(Res.string.label_select_file) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(Res.string.label_select_file)) + } + } + }, + confirmButton = { + TextButton(onClick = onImport) { + Text(stringResource(Res.string.import)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(Res.string.cancel)) + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt index 0c59c0c..806df02 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.settings.feedback - +// 反馈屏幕 import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -59,6 +59,11 @@ import taskttl.composeapp.generated.resources.feedback_placeholder import taskttl.composeapp.generated.resources.feedback_type import taskttl.composeapp.generated.resources.title_feedback +/** + * 反馈屏幕 + * @param [onNavigateBack] 上导航返回 + * @param [viewModel] 视图模型 + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun FeedbackScreen( diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt index eb191a8..d6a5a2f 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt @@ -1,8 +1,17 @@ package com.taskttl.presentation.features.settings.feedback - +// 反馈状态 import com.taskttl.core.base.BaseState import com.taskttl.data.source.remote.dto.request.FeedbackReq +/** + * 反馈状态 + * @author admin + * @date 2025/11/26 + * @constructor 创建[FeedbackState] + * @param [isLoading] 正在加载 + * @param [isProcessing] 正在处理 + * @param [error] 错误 + */ data class FeedbackState( override val isLoading: Boolean = false, override val isProcessing: Boolean = false, diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt index d5c6fb4..185f23e 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.settings.main - +// 设置屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -38,13 +37,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import com.taskttl.core.auth.AuthManager import com.taskttl.core.manager.ThemeMode import com.taskttl.core.permission.NotificationPermissionManager import com.taskttl.navigation.Routes @@ -80,6 +78,7 @@ import taskttl.composeapp.generated.resources.title_app_settings /** * 设置屏幕 * @param [navController] 导航控制器 + * @param [viewModel] 视图模型 */ @Composable fun SettingsScreen( @@ -123,22 +122,27 @@ fun SettingsScreen( ) { // 用户信息卡片(登录前/后通用组件) UserInfoCard( - isLoggedIn = true, - userName = "DevTTL", - userSubtitle = "111111", - onClick = { navController.navigate(Routes.Main.Settings.Login) } + isLoggedIn = !AuthManager.isExpired(), + userInfo = state.userInfo, + onClick = { + if (AuthManager.isExpired()) { + navController.navigate(Routes.User.Login) + } else { + navController.navigate(Routes.User.UserProfile) + } + } ) - Spacer(modifier = Modifier.height(12.dp)) - - Card( - modifier = Modifier.fillMaxWidth().height(80.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ) - ) { - - } + // Spacer(modifier = Modifier.height(12.dp)) + // + // Card( + // modifier = Modifier.fillMaxWidth().height(80.dp), + // colors = CardDefaults.cardColors( + // containerColor = MaterialTheme.colorScheme.surface + // ) + // ) { + // + // } Spacer(modifier = Modifier.height(24.dp)) diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt index f12efd6..463f15f 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt @@ -1,8 +1,10 @@ package com.taskttl.presentation.features.settings.main - +// 设置状态 +import com.taskttl.core.auth.AuthManager import com.taskttl.core.base.BaseState import com.taskttl.core.manager.ThemeMode import com.taskttl.core.permission.NotificationPermissionManager +import com.taskttl.domain.model.UserInfo /** * 设置状态 @@ -16,6 +18,7 @@ data class SettingsState( override val isLoading: Boolean = false, override val isProcessing: Boolean = false, override val error: String? = null, + val userInfo: UserInfo? = AuthManager.userInfo.value, val themeMode: ThemeMode = ThemeMode.SYSTEM, val showThemeDialog: Boolean = false, val isNotification: Boolean = NotificationPermissionManager.verifyPermission(), diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt index 0353dd2..007014c 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt @@ -2,6 +2,7 @@ package com.taskttl.presentation.features.settings.main import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope +import com.taskttl.core.auth.AuthManager import com.taskttl.core.base.BaseViewModel import com.taskttl.core.manager.ThemeManager import com.taskttl.core.manager.ThemeMode @@ -11,10 +12,11 @@ import com.taskttl.core.utils.ExternalAppLauncher import kotlinx.coroutines.launch /** - * 反馈视图模型 + * 设置视图模型 * @author admin - * @date 2025/10/12 + * @date 2025/11/26 * @constructor 创建[SettingsViewModel] + * @param [themeManager] 主题管理器 */ @Stable class SettingsViewModel(private val themeManager: ThemeManager) : @@ -25,6 +27,10 @@ class SettingsViewModel(private val themeManager: ThemeManager) : viewModelScope.launch { themeManager.themeMode.collect { mode -> updateState { copy(themeMode = mode) } } } + + viewModelScope.launch { + AuthManager.userInfo.collect { userInfo -> updateState { copy(userInfo = userInfo) } } + } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt index 107ac02..e63bac1 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.settings.main.common - +// 用户信息卡片 import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -13,9 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -24,27 +22,28 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil3.compose.AsyncImage +import com.taskttl.domain.model.UserInfo import com.taskttl.presentation.common.components.NetworkImage -import taskttl.composeapp.generated.resources.Res /** * 用户信息卡片 * 支持: * - 未登录:展示登录引导 * - 已登录:展示头像首字母、昵称、副标题 + * 用户信息卡 + * @param [isLoggedIn] 已登录 + * @param [userInfo] 用户信息 + * @param [onClick] 点击时 */ @Composable fun UserInfoCard( isLoggedIn: Boolean, - userName: String?, - userSubtitle: String?, + userInfo: UserInfo?, onClick: () -> Unit, ) { @@ -61,15 +60,15 @@ fun UserInfoCard( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.SpaceBetween ) { - if (isLoggedIn && !userName.isNullOrBlank()) { + if (isLoggedIn && userInfo != null) { Text( - text = userName, + text = userInfo.nickname, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 18.sp ) Text( - text = userSubtitle ?: "欢迎回来,继续保持高效!", + text = userInfo.email, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp ) @@ -88,14 +87,6 @@ fun UserInfoCard( } } - if (!isLoggedIn) { - Icon( - imageVector = Icons.Default.ChevronRight, - contentDescription = null, - tint = Color.White.copy(alpha = 0.9f) - ) - } - Spacer(modifier = Modifier.width(12.dp)) // 头像/占位 @@ -105,9 +96,9 @@ fun UserInfoCard( .background(Color.White.copy(alpha = 0.2f), shape = CircleShape), contentAlignment = Alignment.Center ) { - if (isLoggedIn && !userName.isNullOrBlank()) { + if (isLoggedIn && userInfo != null) { NetworkImage( - url = Res.getUri("drawable/ic_launcher.png"), + url = userInfo.avatar, contentDescription = null, modifier = Modifier.size(60.dp), contentScale = ContentScale.Fit @@ -116,7 +107,7 @@ fun UserInfoCard( Icon( imageVector = Icons.Default.Person, contentDescription = "用户头像", - tint = Color.White, + tint = Color.Black, modifier = Modifier.size(40.dp) ) } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt index a96162e..26bc2a8 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.settings.privacy - +// 隐私屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,6 +14,10 @@ import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.privacy_url import taskttl.composeapp.generated.resources.title_privacy +/** + * 隐私屏幕 + * @param [onNavigateBack] 上导航返回 + */ @Composable fun PrivacyScreen(onNavigateBack: () -> Unit) { Box(modifier = Modifier.fillMaxSize()) { diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt index 495cd2a..5eee6fc 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.splash - +// 启动视图 import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -53,13 +53,8 @@ fun SplashScreen( LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { - is SplashEffect.NavigateToOnboarding -> { - navigatorToRoute(Routes.Onboarding) - } - - is SplashEffect.NavigateToMain -> { - navigatorToRoute(Routes.Main) - } + is SplashEffect.NavigateToOnboarding -> navigatorToRoute(Routes.Onboarding) + is SplashEffect.NavigateToMain -> navigatorToRoute(Routes.Main) } } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt index 0defe63..2e47835 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.splash - +// 启动页状态 import com.taskttl.core.base.BaseState diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt index 9f2bffc..5376c55 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt @@ -44,9 +44,15 @@ class SplashViewModel( */ private fun loadingStatus() { viewModelScope.launch { - delay(200) + DeviceUtils.getUniqueId() + val firstResult = StorageUtils.getBoolean(PointEvent.FirstAppLaunch.eventName, false) + if (!firstResult) { + TaskTTLApi.postPoint(PointEvent.FirstAppLaunch) + StorageUtils.saveBoolean(PointEvent.FirstAppLaunch.eventName, true) + } else { + TaskTTLApi.postPoint(PointEvent.AppLaunch) + } val hasLaunched = onboardingRepository.isLaunchedBefore() - LogUtils.e("DevTTL",hasLaunched.toString()) withContext(Dispatchers.Main) { if (hasLaunched) { sendEvent(SplashEffect.NavigateToOnboarding) diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt index 171e48a..02e2449 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt @@ -1,8 +1,8 @@ package com.taskttl.presentation.features.statistics +// 统计屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,24 +10,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Assignment import androidx.compose.material.icons.automirrored.filled.TrendingUp import androidx.compose.material.icons.filled.CalendarToday import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Schedule -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -36,21 +26,17 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.taskttl.domain.model.Category import com.taskttl.navigation.Routes import com.taskttl.presentation.common.components.AppHeader -import com.taskttl.presentation.common.components.Chip import com.taskttl.presentation.features.countdown.list.CountdownIntent import com.taskttl.presentation.features.countdown.list.CountdownViewModel +import com.taskttl.presentation.features.statistics.components.CategoryStatisticItem +import com.taskttl.presentation.features.statistics.components.StatisticCard import com.taskttl.presentation.features.task.list.TaskIntent import com.taskttl.presentation.features.task.list.TaskViewModel -import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res @@ -64,6 +50,12 @@ import taskttl.composeapp.generated.resources.setting_category_management import taskttl.composeapp.generated.resources.title_statistics import taskttl.composeapp.generated.resources.total_tasks +/** + * 统计屏幕 + * @param [navController] 导航控制器 + * @param [taskViewModel] 任务视图模型 + * @param [countdownViewModel] 倒计时视图模型 + */ @Composable fun StatisticsScreen( navController: NavHostController, @@ -209,142 +201,3 @@ fun StatisticsScreen( } } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun StatisticCard( - titleRes: StringResource, - value: String, - icon: ImageVector, - color: Color, - modifier: Modifier = Modifier, -) { - Card( - modifier = modifier, - colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f)) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = icon, - contentDescription = stringResource(titleRes), - tint = color, - modifier = Modifier.size(32.dp) - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = value, - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = color - ) - - Text( - text = stringResource(titleRes), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun CategoryStatisticItem( - category: Category, - totalCount: Int, - completedCount: Int, - typeRes: StringResource, -) { - if (totalCount == 0) return - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 分类颜色指示器 - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(category.color.backgroundColor), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = category.icon.icon, - contentDescription = stringResource(category.icon.displayNameRes), - tint = category.color.iconColor, - modifier = Modifier.size(24.dp) - ) - } - - Spacer(modifier = Modifier.width(16.dp)) - - // 分类信息 - Column( - modifier = Modifier.weight(1f) - ) { - Row() { - Text( - text = category.name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = category.color.textColor - ) - Spacer(modifier = Modifier.width(6.dp)) - - Chip( - textRes = category.type.displayNameRes, - ) - } - - Text( - text = "${stringResource(typeRes)}: $completedCount/$totalCount", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // 进度条 - Column( - horizontalAlignment = Alignment.End - ) { - val progress = if (totalCount > 0) completedCount.toFloat() / totalCount else 0f - - Text( - text = "${(progress * 100).toInt()}%", - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Medium, - color = category.color.textColor - ) - - Spacer(modifier = Modifier.height(4.dp)) - - LinearProgressIndicator( - progress = { progress }, - modifier = Modifier - .width(80.dp) - .height(6.dp) - .clip(RoundedCornerShape(3.dp)), - color = category.color.textColor, - trackColor = ProgressIndicatorDefaults.linearTrackColor, - strokeCap = ProgressIndicatorDefaults.LinearStrokeCap, - ) - } - } - } - Spacer(modifier = Modifier.height(10.dp)) -} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt new file mode 100644 index 0000000..b179de8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt @@ -0,0 +1,134 @@ +package com.taskttl.presentation.features.statistics.components +// 类别统计项 +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.taskttl.domain.model.Category +import com.taskttl.presentation.common.components.Chip +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource + + +/** + * 类别统计项 + * @param [category] 类别 + * @param [totalCount] 总数 + * @param [completedCount] 已完成计数 + * @param [typeRes] 类型res + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CategoryStatisticItem( + category: Category, + totalCount: Int, + completedCount: Int, + typeRes: StringResource, +) { + if (totalCount == 0) return + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 分类颜色指示器 + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(category.color.backgroundColor), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = category.icon.icon, + contentDescription = stringResource(category.icon.displayNameRes), + tint = category.color.iconColor, + modifier = Modifier.size(24.dp) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + // 分类信息 + Column( + modifier = Modifier.weight(1f) + ) { + Row() { + Text( + text = category.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = category.color.textColor + ) + Spacer(modifier = Modifier.width(6.dp)) + + Chip( + textRes = category.type.displayNameRes, + ) + } + + Text( + text = "${stringResource(typeRes)}: $completedCount/$totalCount", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // 进度条 + Column( + horizontalAlignment = Alignment.End + ) { + val progress = if (totalCount > 0) completedCount.toFloat() / totalCount else 0f + + Text( + text = "${(progress * 100).toInt()}%", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + color = category.color.textColor + ) + + Spacer(modifier = Modifier.height(4.dp)) + + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .width(80.dp) + .height(6.dp) + .clip(RoundedCornerShape(3.dp)), + color = category.color.textColor, + trackColor = ProgressIndicatorDefaults.linearTrackColor, + strokeCap = ProgressIndicatorDefaults.LinearStrokeCap, + ) + } + } + } + Spacer(modifier = Modifier.height(10.dp)) +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt new file mode 100644 index 0000000..bbf0484 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt @@ -0,0 +1,76 @@ +package com.taskttl.presentation.features.statistics.components +// 统计卡 +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource + + +/** + * 统计卡 + * @param [titleRes] 标题res + * @param [value] 价值 + * @param [icon] 图标 + * @param [color] 颜色 + * @param [modifier] 修饰符 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StatisticCard( + titleRes: StringResource, + value: String, + icon: ImageVector, + color: Color, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = stringResource(titleRes), + tint = color, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = value, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = color + ) + + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt index 475f0b9..add800d 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt @@ -34,6 +34,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import com.taskttl.core.ui.component.empty.Empty +import com.taskttl.core.ui.component.empty.EmptyData import com.taskttl.core.utils.ToastUtils import com.taskttl.navigation.Routes import com.taskttl.presentation.common.components.AppHeader @@ -167,23 +169,10 @@ fun TaskScreen( } state.filteredTasks.isEmpty() -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(Res.string.text_no_tasks), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(Res.string.text_add_task_hint), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + Empty( + message = Res.string.text_no_tasks, + subtitle = Res.string.text_add_task_hint + ) } else -> { diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt index 9b65725..756c586 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt @@ -149,6 +149,12 @@ sealed class TaskIntent { * @constructor 创建[TaskEffect] */ sealed class TaskEffect { + /** + * 导航返回 + * @author admin + * @date 2025/09/27 + */ + object NavigateBack : TaskEffect() /** * 显示消息 * @author admin @@ -173,11 +179,4 @@ sealed class TaskEffect { * @date 2025/09/27 */ data object NavigateToEditTask : TaskEffect() - - /** - * 导航返回 - * @author admin - * @date 2025/09/27 - */ - object NavigateBack : TaskEffect() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginScreen.kt similarity index 53% rename from composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt rename to composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginScreen.kt index 6550a0e..06be901 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginScreen.kt @@ -1,14 +1,11 @@ -package com.taskttl.presentation.features.auth - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image +package com.taskttl.presentation.features.user.login +// 登录屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -21,10 +18,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Facebook import androidx.compose.material.icons.filled.Language +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -39,17 +37,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.taskttl.core.utils.ToastUtils -import com.taskttl.navigation.Routes import com.taskttl.presentation.common.components.AppHeader import com.taskttl.presentation.common.components.ErrorDialog -import com.taskttl.presentation.features.task.list.TaskEffect -import com.taskttl.presentation.features.task.list.TaskIntent -import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.InternalResourceApi -import org.jetbrains.compose.resources.painterResource import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.app_name @@ -63,23 +55,15 @@ import taskttl.composeapp.generated.resources.app_name @Composable fun LoginScreen( onNavigateBack: () -> Unit, - viewModel: AuthViewModel = koinViewModel(), + viewModel: LoginViewModel = koinViewModel(), ) { - val state by viewModel.state.collectAsState() LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { - is AuthEffect.ShowMessage -> { - ToastUtils.show(effect.message) - } - - // is TaskEffect.NavigateToTaskDetail -> { - // navController.navigate(Routes.Main.Task.TaskDetail(effect.taskId)) - // } - - else -> {} + is LoginEffect.NavigateBack -> onNavigateBack.invoke() + is LoginEffect.ShowMessage -> ToastUtils.show(effect.message) } } } @@ -87,7 +71,7 @@ fun LoginScreen( state.error?.let { error -> ErrorDialog( errorMessage = state.error, - onDismiss = { viewModel.handleIntent(AuthIntent.ClearError) } + onDismiss = { viewModel.handleIntent(LoginIntent.ClearError) } ) } @@ -105,68 +89,86 @@ fun LoginScreen( Spacer(modifier = Modifier.height(48.dp)) - Column( - modifier = Modifier.fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier + .fillMaxSize() ) { - // 应用 Logo - AsyncImage( - model = Res.getUri("drawable/ic_launcher.png"), - contentDescription = null, - modifier = Modifier.size(120.dp), - contentScale = ContentScale.Fit - ) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(28.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AsyncImage( + model = Res.getUri("drawable/ic_launcher.png"), + contentDescription = null, + modifier = Modifier.size(120.dp), + contentScale = ContentScale.Fit + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "TaskTTL 登录", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) + Text( + text = "TaskTTL 登录", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "欢迎回来,请选择登录方式:", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + Text( + text = "欢迎回来,请选择登录方式。", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(24.dp)) - // Google 登录按钮 - LoginButton( - icon = Icons.Default.Language, - text = "使用 Google 登录", - backgroundColor = Color(0xFFDB4437), - onClick = { viewModel.handleIntent(AuthIntent.LoginWithGoogle) } - ) + LoginButton( + icon = Icons.Default.Language, + text = "使用 Google 登录", + backgroundColor = Color(0xFFDB4437), + onClick = { viewModel.handleIntent(LoginIntent.LoginWithGoogle) } + ) - Spacer(modifier = Modifier.height(16.dp)) - - // Facebook 登录按钮 - LoginButton( - icon = Icons.Default.Facebook, - text = "使用 Facebook 登录", - backgroundColor = Color(0xFF1877F2), - onClick = { viewModel.handleIntent(AuthIntent.LoginWithFacebook) } - ) - - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(16.dp)) + LoginButton( + icon = Icons.Default.Facebook, + text = "使用 Facebook 登录", + backgroundColor = Color(0xFF1877F2), + onClick = { viewModel.handleIntent(LoginIntent.LoginWithFacebook) } + ) + } + } Text( text = "© DevTTL Team. All rights reserved.", style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, - modifier = Modifier.padding(bottom = 8.dp) + modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 32.dp) + .fillMaxWidth() ) } + } + if (state.isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.35f)), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } } } } @@ -208,38 +210,3 @@ private fun LoginButton( ) } } - -@Composable -private fun AuthOutlineButton( - text: String, - onClick: () -> Unit, - iconRes: DrawableResource, - modifier: Modifier = Modifier, -) { - OutlinedButton( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - contentPadding = PaddingValues(16.dp), - border = BorderStroke(width = 1.dp, color = Color.Black), - onClick = onClick - ) { - Row( - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Image( - painter = painterResource(iconRes), - modifier = Modifier.size(24.dp), - contentDescription = "", - ) - Text( - text = text, - modifier = Modifier - .padding(start = 12.dp) - .align(Alignment.CenterVertically), - fontSize = 16.sp, - color = Color.Black, - ) - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginState.kt similarity index 57% rename from composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt rename to composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginState.kt index 01f1878..884dcaf 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginState.kt @@ -1,19 +1,18 @@ -package com.taskttl.presentation.features.auth - +package com.taskttl.presentation.features.user.login +// 身份验证状态 import com.taskttl.core.base.BaseState -import com.taskttl.presentation.features.task.list.TaskEffect /** * 身份验证状态 * @author admin * @date 2025/10/26 - * @constructor 创建[AuthState] + * @constructor 创建[LoginState] * @param [isLoading] 正在加载 * @param [isProcessing] 正在处理 * @param [error] 错误 */ -data class AuthState( +data class LoginState( override val isLoading: Boolean = false, override val isProcessing: Boolean = false, override val error: String? = null, @@ -24,35 +23,42 @@ data class AuthState( * 身份验证意图 * @author admin * @date 2025/10/26 - * @constructor 创建[AuthIntent] + * @constructor 创建[LoginIntent] */ -sealed class AuthIntent { +sealed class LoginIntent { /** * 使用谷歌登录 * @author admin * @date 2025/10/26 */ - object LoginWithGoogle: AuthIntent() + object LoginWithGoogle : LoginIntent() /** * 使用脸书登录 * @author admin * @date 2025/10/26 */ - object LoginWithFacebook: AuthIntent() + object LoginWithFacebook : LoginIntent() - object ClearError: AuthIntent() + object ClearError : LoginIntent() - object Logout : AuthIntent() + object Logout : LoginIntent() } /** * 身份验证效果 * @author admin * @date 2025/10/26 - * @constructor 创建[AuthEffect] + * @constructor 创建[LoginEffect] */ -sealed class AuthEffect { +sealed class LoginEffect { + /** + * 导航返回 + * @author admin + * @date 2025/11/25 + */ + object NavigateBack : LoginEffect() + /** * 显示消息 * @author admin @@ -60,5 +66,5 @@ sealed class AuthEffect { * @constructor 创建[ShowMessage] * @param [message] 消息 */ - data class ShowMessage(val message: String) : AuthEffect() + data class ShowMessage(val message: String) : LoginEffect() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt new file mode 100644 index 0000000..99110d0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt @@ -0,0 +1,104 @@ +package com.taskttl.presentation.features.user.login + +import androidx.compose.runtime.Stable +import androidx.lifecycle.viewModelScope +import com.taskttl.core.auth.AuthManager +import com.taskttl.core.base.BaseViewModel +import com.taskttl.data.source.remote.api.TaskTTLApi +import com.taskttl.data.source.remote.dto.response.AuthResult +import com.taskttl.domain.repository.AuthRepository +import kotlinx.coroutines.launch +import kotlin.time.ExperimentalTime + +/** + * 身份验证视图模型 + * @author admin + * @date 2025/10/26 + * @constructor 创建[LoginViewModel] + */ +@Stable +class LoginViewModel(private val authRepository: AuthRepository) : + BaseViewModel(LoginState()) { + + + public override fun handleIntent(intent: LoginIntent) { + when (intent) { + is LoginIntent.LoginWithGoogle -> onLoginWithGoogle() + is LoginIntent.LoginWithFacebook -> onLoginWithFacebook() + is LoginIntent.ClearError -> clearError() + is LoginIntent.Logout -> {} + } + } + + private fun setLoading(loading: Boolean) { + updateState { copy(isLoading = loading, isProcessing = false, error = null) } + } + + /** + * 清除错误 + */ + private fun clearError() { + updateState { copy(error = null) } + } + + /** + * 统一错误展示 + */ + private fun showError(message: String) { + updateState { copy(error = message) } + sendEvent(LoginEffect.ShowMessage(message)) + } + + /** + * 使用谷歌登录 + */ + private fun onLoginWithGoogle() { + if (state.value.isLoading) return + setLoading(true) + viewModelScope.launch { + try { + when (val result = authRepository.loginWithGoogle()) { + is AuthResult.Success -> handleGoogleLoginSuccess(result) + is AuthResult.Canceled -> showError("Login canceled") + is AuthResult.Error -> showError(result.message ?: "Unknown error") + } + } finally { + setLoading(false) + } + } + } + + /** + * 使用脸书登录 + */ + private fun onLoginWithFacebook() { + logDebug("Facebook 登录触发") + viewModelScope.launch { + val result = authRepository.loginWithFacebook() + } + } + + /** + * 处理谷歌登录成功 + * @param [authResult] 登录结果 + */ + @OptIn(ExperimentalTime::class) + private suspend fun handleGoogleLoginSuccess(authResult: AuthResult.Success) { + if (authResult.account.isEmpty) { + showError("Login canceled") + return + } + runCatching { + TaskTTLApi.thirdPartyLogin(authResult.account) + }.onSuccess { result -> + AuthManager.updateToken(result.toTokenInfo()) + AuthManager.updateUserInfo(result.toUserInfo()) + sendEvent(LoginEffect.ShowMessage("登录成功")) + sendEvent(LoginEffect.NavigateBack) + }.onFailure { e -> + val message = e.message ?: "登录失败,请稍后重试" + logDebug("Third party login failed: $message") + showError(message) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt new file mode 100644 index 0000000..c0575eb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt @@ -0,0 +1,241 @@ +package com.taskttl.presentation.features.user.userProfile + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.WarningAmber +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.taskttl.core.utils.ToastUtils +import com.taskttl.presentation.common.components.AppHeader +import com.taskttl.presentation.features.user.userProfile.components.AvatarView +import com.taskttl.presentation.features.user.userProfile.components.EmailRow +import com.taskttl.presentation.features.user.userProfile.components.InfoRow +import com.taskttl.presentation.features.user.userProfile.components.NicknameEditor +import org.koin.compose.viewmodel.koinViewModel +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.title_app_settings + +@Composable +fun UserProfileScreen( + onNavigateBack: () -> Unit, + viewModel: UserProfileViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsState() + + if (state.userInfo == null) { + return + } + + LaunchedEffect(Unit) { + viewModel.effects.collect { effect -> + when (effect) { + is UserProfileEffect.NavigateBack -> onNavigateBack.invoke() + is UserProfileEffect.ShowMessage -> ToastUtils.show(effect.message) + } + } + } + + val clipboardManager = LocalClipboardManager.current + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + AppHeader( + title = Res.string.title_app_settings, + showBack = true, + onBackClick = { onNavigateBack.invoke() }, + trailingIcon = Icons.Default.Person, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) + ) { + // 顶部用户卡片 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = RoundedCornerShape(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 头像 + AvatarView( + name = state.userInfo!!.nickname, + avatarUrl = state.userInfo?.avatar, + onClick = { viewModel.handleIntent(UserProfileIntent.ChangeAvatar) } + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 昵称编辑 + NicknameEditor( + nickname = state.userInfo!!.nickname, + onNicknameChanged = { + viewModel.handleIntent(UserProfileIntent.UpdateNickname(it)) + }, + onSave = { + viewModel.handleIntent(UserProfileIntent.SaveNickname) + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 邮箱展示 + EmailRow(email = state.userInfo!!.email) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 详细信息列表(ID / 账号信息) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = RoundedCornerShape(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = "账户信息", + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(12.dp)) + + // 用户 ID + InfoRow( + label = "用户 ID", + value = state.userInfo!!.userId.toString(), + trailing = { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = "复制用户 ID", + modifier = Modifier + .size(18.dp) + .clickable { + // clipboardManager.setText(annotatedString) + viewModel.handleIntent(UserProfileIntent.ShowToast("已复制用户 ID")) + }, + tint = LocalContentColor.current + ) + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 用户昵称(只展示,真正编辑在上面) + InfoRow( + label = "用户昵称", + value = state.userInfo!!.nickname + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 邮箱 + InfoRow( + label = "邮箱", + value = state.userInfo!!.email + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + // 底部操作按钮 + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = { viewModel.handleIntent(UserProfileIntent.Logout) }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Logout, + contentDescription = null + ) + Spacer(modifier = Modifier.size(8.dp)) + Text("退出登录") + } + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedButton( + onClick = { + viewModel.handleIntent(UserProfileIntent.DeleteAccount) + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Icon( + imageVector = Icons.Default.WarningAmber, + contentDescription = null + ) + Spacer(modifier = Modifier.size(8.dp)) + Text("删除账号") + } + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt new file mode 100644 index 0000000..9381dc4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt @@ -0,0 +1,55 @@ +package com.taskttl.presentation.features.user.userProfile + +import com.taskttl.core.auth.AuthManager +import com.taskttl.core.base.BaseState +import com.taskttl.domain.model.UserInfo + +/** + * 用户配置文件状态 + * @author DevTTL + * @date 2025/12/03 + * @constructor 创建[UserProfileState] + * @param [userInfo] 用户信息 + */ +data class UserProfileState( + val userInfo: UserInfo? = AuthManager.userInfo.value, +): BaseState() + +/** + * 用户配置文件意图 + * @author DevTTL + * @date 2025/12/03 + * @constructor 创建[UserProfileIntent] + */ +sealed interface UserProfileIntent { + data class UpdateNickname(val value: String) : UserProfileIntent + data object SaveNickname : UserProfileIntent + data object ChangeAvatar : UserProfileIntent + data object Logout : UserProfileIntent + data object DeleteAccount : UserProfileIntent + data class ShowToast(val message: String) : UserProfileIntent +} + +/** + * 用户配置文件效果 + * @author DevTTL + * @date 2025/12/03 + * @constructor 创建[UserProfileEffect] + */ +sealed interface UserProfileEffect { + /** + * 导航返回 + * @author admin + * @date 2025/09/27 + */ + object NavigateBack : UserProfileEffect + + /** + * 显示消息 + * @author admin + * @date 2025/09/27 + * @constructor 创建[ShowMessage] + * @param [message] 消息 + */ + data class ShowMessage(val message: String) : UserProfileEffect +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt new file mode 100644 index 0000000..f0d1315 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt @@ -0,0 +1,37 @@ +package com.taskttl.presentation.features.user.userProfile + +import androidx.compose.runtime.Stable +import androidx.lifecycle.viewModelScope +import com.taskttl.core.auth.AuthManager +import com.taskttl.core.base.BaseViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * 用户配置文件视图模型 + * @author DevTTL + * @date 2025/12/03 + * @constructor 创建[UserProfileViewModel] + */ +@Stable +class UserProfileViewModel() : + BaseViewModel(UserProfileState()) { + + init { + viewModelScope.launch(Dispatchers.Default) { + AuthManager.userInfo.collect { updateState { copy(userInfo = it) } } + } + } + + public override fun handleIntent(intent: UserProfileIntent) { + when (intent) { + is UserProfileIntent.Logout -> {} + is UserProfileIntent.ShowToast -> {} + is UserProfileIntent.UpdateNickname -> {} + is UserProfileIntent.DeleteAccount -> {} + is UserProfileIntent.ChangeAvatar -> {} + is UserProfileIntent.SaveNickname -> {} + } + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt new file mode 100644 index 0000000..e284aee --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt @@ -0,0 +1,66 @@ +package com.taskttl.presentation.features.user.userProfile.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * 头像视图 + * @param [name] 名字 + * @param [avatarUrl] 头像链接 + * @param [onClick] 点击时 + */ +@Composable +fun AvatarView( + name: String, + avatarUrl: String?, + onClick: () -> Unit, +) { + Box( + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + // 如果你有真实头像,这里改成 Image(...) + Text( + text = name.takeIf { it.isNotBlank() }?.first()?.uppercase() ?: "U", + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + + // 右下角一个小铅笔提示可编辑 + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(24.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "编辑头像", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt new file mode 100644 index 0000000..8eb5e31 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt @@ -0,0 +1,41 @@ +package com.taskttl.presentation.features.user.userProfile.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * 电子邮件行 + * @param [email] 电子邮件 + */ +@Composable +fun EmailRow(email: String) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Email, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.size(4.dp)) + Text( + text = email.ifBlank { "未绑定邮箱" }, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt new file mode 100644 index 0000000..796ec3c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt @@ -0,0 +1,57 @@ +package com.taskttl.presentation.features.user.userProfile.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * 信息行 + * @param [label] 标签 + * @param [value] 价值 + * @param [trailing] 拖尾 + */ +@Composable +fun InfoRow( + label: String, + value: String, + trailing: (@Composable () -> Unit)? = null, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = value.ifBlank { "-" }, + fontSize = 15.sp, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (trailing != null) { + Spacer(modifier = Modifier.size(8.dp)) + trailing() + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt new file mode 100644 index 0000000..30fff85 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt @@ -0,0 +1,55 @@ +package com.taskttl.presentation.features.user.userProfile.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +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 + +/** + * 昵称编辑器 + * @param [nickname] 昵称 + * @param [onNicknameChanged] 昵称已更改 + * @param [onSave] 关于保存 + */ +@Composable +fun NicknameEditor( + nickname: String, + onNicknameChanged: (String) -> Unit, + onSave: () -> Unit, +) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + value = nickname, + onValueChange = onNicknameChanged, + singleLine = true, + label = { Text("昵称") }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null + ) + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = onSave, + shape = RoundedCornerShape(999.dp), + contentPadding = ButtonDefaults.ContentPadding + ) { + Text("保存昵称") + } + } +} diff --git a/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt b/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt index a60e213..d5f1f00 100644 --- a/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt @@ -1,5 +1,6 @@ package com.taskttl import androidx.compose.ui.window.ComposeUIViewController +import com.taskttl.app.App fun MainViewController() = ComposeUIViewController { App() } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35206f9..55984e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,41 +1,50 @@ [versions] -agp = "8.13.1" -androidx-activity = "1.11.0" +agp = "8.13.2" +androidx-activity = "1.12.4" androidx-appcompat = "1.7.1" androidx-constraintlayout = "2.2.1" androidx-core = "1.17.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.6" androidx-testExt = "1.3.0" -composeHotReload = "1.0.0-rc02" -composeMultiplatform = "1.10.0-beta01" +componentsResources = "1.11.0-alpha02" +composeHotReload = "1.0.0" +composeMultiplatform = "1.11.0-alpha02" +foundation = "1.11.0-alpha02" junit = "4.13.2" -kotlin = "2.2.21" +kotlin = "2.3.10" kotlinx-coroutines = "1.10.2" +material3 = "1.9.0" +runtime = "1.11.0-alpha02" splashscreen = "1.2.0" -navigationCompose = "2.9.1" -koin = "4.1.1" -ktor = "3.3.2" +navigationCompose = "2.9.2" +androidxNavigation3UI = "1.0.0-alpha04" +androidxNavigation3Material = "1.3.0-alpha01" +koin = "4.2.0-RC1" +ktor = "3.4.0" coil3 = "3.3.0" kotlinx-datetime = "0.7.1" icons = "1.7.3" google = "4.4.4" -credentials = "1.6.0-beta03" -googleid = "1.1.1" -firebase = "34.6.0" +credentials = "1.6.0-rc01" +googleid = "1.2.0" +firebase = "34.9.0" facebook = "18.1.3" -playServicesAds = "18.2.0" -kotlinx-serialization = "1.9.0" +identifier = "1.0.0-alpha05" +kotlinx-serialization = "1.10.0" settings = "1.3.0" -sqlite = "2.6.1" -room = "2.8.3" -ksp = "2.3.0" +sqlite = "2.6.2" +room = "2.8.4" +ksp = "2.3.2" -work = "2.11.0" +ui = "1.11.0-alpha02" +uiTooling = "1.11.0-alpha02" +uiToolingPreviewVersion = "1.11.0-alpha02" +work = "2.11.1" # 环境 config-appName = "TaskTTL" @@ -54,6 +63,12 @@ android-facebookClientToken = "15db47cd9d8d35ccaa49f43e30beefaf" [libraries] +components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "componentsResources" } +foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "foundation" } +jetbrains-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreviewVersion" } +runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "runtime" } +ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "ui" } +ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "uiTooling" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -72,13 +87,17 @@ kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-s androidx-splashscreen = {module = "androidx.core:core-splashscreen" , version.ref = "splashscreen"} # 导航 +material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +#androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "androidxNavigation3UI" } +#androidx-navigation3-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "androidxNavigation3Material" } # koin 依赖注入 koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-navigation3 = {module = "io.insert-koin:koin-compose-navigation3", version.ref = "koin"} # ktor 网络请求 ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } @@ -92,7 +111,7 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k # coil3 coil3-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" } coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" } -coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" } +#coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" } coil3-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3" } # 时间 @@ -123,7 +142,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" } # 谷歌Ads -android-play-services-ads-identifier = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" } +androidx-ads-identifier = { module = "androidx.ads:ads-identifier", version.ref = "identifier" } # 安卓任务 androidx-work = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }