From 837ac187e3c321467ada580a66950781f17926fb Mon Sep 17 00:00:00 2001 From: devttl Date: Sat, 15 Nov 2025 20:23:59 +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 | 16 +- .../src/androidMain/AndroidManifest.xml | 5 +- .../kotlin/com/taskttl/MainActivity.kt | 38 +-- .../kotlin/com/taskttl/MainApplication.kt | 2 - .../com/taskttl/app/di/AppModule.android.kt | 6 + .../core/common/DevTTLWebView.android.kt | 22 +- .../AppNotificationManager.android.kt | 62 ++++- .../core/{alarm => receiver}/AlarmReceiver.kt | 2 +- .../core/{alarm => receiver}/BootReceiver.kt | 2 +- .../taskttl/core/utils/DeviceUtils.android.kt | 126 ++++----- .../core/utils/StorageUtils.android.kt | 80 ------ .../repository/AuthRepository.android.kt | 106 -------- .../domain/repository/AuthRepositoryImpl.kt | 92 +++++++ .../foundation/ImageLoaderProvider.android.kt | 16 ++ .../src/androidMain/res/values-v31/styles.xml | 18 ++ .../src/androidMain/res/values/styles.xml | 12 + .../commonMain/kotlin/com/taskttl/app/App.kt | 4 +- .../kotlin/com/taskttl/app/AppState.kt | 23 ++ .../kotlin/com/taskttl/app/AppViewModel.kt | 22 +- .../kotlin/com/taskttl/app/di/DataModels.kt | 12 +- .../kotlin/com/taskttl/app/di/KoinModels.kt | 4 +- .../com/taskttl/core/common/DevTTLWebView.kt | 2 +- .../com/taskttl/core/manager/ThemeManager.kt | 1 - .../com/taskttl/core/network/ApiConfig.kt | 6 + .../com/taskttl/core/utils/StorageUtils.kt | 61 ++++- .../com/taskttl/data/constant/Constant.kt | 6 + .../com/taskttl/data/constant/ProviderEnum.kt | 14 + .../data/source/remote/api/TaskTTLApi.kt | 18 +- .../source/remote/dto/request/LoginReq.kt | 24 ++ .../source/remote/dto/response/AuthResult.kt | 23 +- .../domain/repository/AuthRepository.kt | 18 +- .../kotlin/com/taskttl/navigation/AppNav.kt | 2 +- .../kotlin/com/taskttl/navigation/MainNav.kt | 13 +- .../kotlin/com/taskttl/navigation/Routes.kt | 109 +++++++- .../common/components/LoadingOverlay.kt | 13 - .../common/components/NetworkImage.kt | 37 +++ .../common/foundation/GlobalImageLoader.kt | 50 ++++ .../presentation/features/auth/AuthState.kt | 16 +- .../features/auth/AuthViewModel.kt | 77 +++++- .../presentation/features/auth/LoginScreen.kt | 255 +++++++++--------- .../category/list/CategoryViewModel.kt | 2 + .../countdown/list/CountdownScreen.kt | 2 - .../countdown/list/CountdownViewModel.kt | 2 + .../features/onboarding/OnboardingScreen.kt | 2 - .../features/onboarding/OnboardingState.kt | 18 ++ .../onboarding/OnboardingViewModel.kt | 26 ++ .../features/settings/about/AboutScreen.kt | 48 +--- .../settings/feedback/FeedbackViewModel.kt | 2 + .../features/settings/main/SettingsScreen.kt | 105 ++------ .../settings/main/SettingsViewModel.kt | 2 + .../settings/main/common/UserInfoCard.kt | 126 +++++++++ .../features/splash/SplashScreen.kt | 2 - .../features/splash/SplashState.kt | 7 +- .../features/splash/SplashViewModel.kt | 41 +-- .../statistics/CategoryStatisticsCard.kt | 177 ------------ .../features/statistics/StatisticsScreen.kt | 2 - .../features/task/detail/TaskEditorScreen.kt | 35 ++- .../features/task/editor/TaskDetailScreen.kt | 8 + .../features/task/list/TaskScreen.kt | 9 +- .../features/task/list/TaskState.kt | 3 +- .../features/task/list/TaskViewModel.kt | 2 + .../features/task/list/components/TaskCard.kt | 1 + composeApp/src/google-services.json | 19 ++ .../com/taskttl/app/di/AppModule.jvm.kt | 9 + .../taskttl/core/common/DevTTLWebView.jvm.kt | 8 + .../taskttl/core/database/Database.desktop.kt | 14 + .../ExactAlarmPermissionManager.jvm.kt | 2 + .../NotificationPermissionManager.jvm.kt | 2 + .../com/taskttl/core/ui/DevTTLWebView.jvm.kt | 8 - .../com/taskttl/core/utils/DeviceUtils.jvm.kt | 13 +- .../taskttl/core/utils/Localization.jvm.kt | 5 +- .../taskttl/core/utils/StorageUtils.jvm.kt | 48 ---- .../com/taskttl/data/di/AppModule.jvm.kt | 9 - .../data/local/database/Database.desktop.kt | 11 - .../data/repository/AuthRepository.jvm.kt | 13 - .../domain/repository/AuthRepository.jvm.kt | 14 + .../src/jvmMain/kotlin/com/taskttl/main.kt | 6 + .../foundation/GlobalImageLoader.jvm.kt | 12 + gradle/libs.versions.toml | 25 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 80 files changed, 1297 insertions(+), 960 deletions(-) rename composeApp/src/androidMain/kotlin/com/taskttl/core/{alarm => receiver}/AlarmReceiver.kt (97%) rename composeApp/src/androidMain/kotlin/com/taskttl/core/{alarm => receiver}/BootReceiver.kt (97%) delete mode 100644 composeApp/src/androidMain/kotlin/com/taskttl/core/utils/StorageUtils.android.kt delete mode 100644 composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepository.android.kt create mode 100644 composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt create mode 100644 composeApp/src/androidMain/kotlin/com/taskttl/presentation/common/foundation/ImageLoaderProvider.android.kt create mode 100644 composeApp/src/androidMain/res/values-v31/styles.xml create mode 100644 composeApp/src/androidMain/res/values/styles.xml create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/constant/ProviderEnum.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/CategoryStatisticsCard.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/app/di/AppModule.jvm.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/core/common/DevTTLWebView.jvm.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/core/database/Database.desktop.kt delete mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/core/ui/DevTTLWebView.jvm.kt delete mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/StorageUtils.jvm.kt delete mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/data/di/AppModule.jvm.kt delete mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/data/local/database/Database.desktop.kt delete mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/data/repository/AuthRepository.jvm.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/domain/repository/AuthRepository.jvm.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.jvm.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index d87377e..1ced809 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -52,6 +52,9 @@ kotlin { implementation(compose.preview) implementation(libs.androidx.activity.compose) + // 启动 + implementation(libs.androidx.splashscreen) + // Koin依赖注入 implementation(libs.koin.android) @@ -64,9 +67,6 @@ kotlin { // facebook implementation(libs.android.facebook.android.sdk) - // mmkv - implementation(libs.android.mmkv) - // sqlite implementation(libs.androidx.room.sqlite.wrapper) @@ -91,6 +91,7 @@ kotlin { implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.lifecycle.savedstate) implementation(libs.androidx.lifecycle.runtimeCompose) @@ -127,6 +128,10 @@ kotlin { // Room implementation(libs.androidx.room.runtime) implementation(libs.androidx.sqlite.bundled) + + // Settings + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.no.arg) } iosMain.dependencies { // ktor网络请求 @@ -138,6 +143,9 @@ kotlin { jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) + + // ktor网络请求 + implementation(libs.ktor.client.java) } commonTest.dependencies { implementation(libs.kotlin.test) @@ -180,6 +188,8 @@ android { getByName("debug") { isMinifyEnabled = false + applicationIdSuffix = ".debug" + buildConfigField("Boolean", "DEBUG", "true") buildConfigField("Integer", "APP_ID", "999") buildConfigField("Integer", "VERSION_CODE", libs.versions.android.versionCode.get()) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index f0df5bc..264168c 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -26,6 +26,7 @@ tools:targetApi="33"> @@ -66,7 +67,7 @@ @@ -77,7 +78,7 @@ diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt index 3658328..61d1fa8 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt @@ -1,29 +1,45 @@ package com.taskttl import android.content.res.Configuration +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat 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 + // splashScreen?.apply { + // // setKeepOnScreenCondition { isChecking } + // } super.onCreate(savedInstanceState) enableEdgeToEdge() // 设置全屏显示 WindowCompat.setDecorFitsSystemWindows(window, false) + setStatusBar() + setContent { + App() + } + } + + /** + * 设置状态栏 + */ + private fun setStatusBar() { // 设置状态栏图标颜色 val controller = WindowInsetsControllerCompat(window, window.decorView) // windowInsetsController.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) - // windowInsetsController.systemBarsBehavior = - // WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + // windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // 显示状态栏和导航栏 // controller.show(WindowInsetsCompat.Type.systemBars()) @@ -32,19 +48,9 @@ class MainActivity : ComponentActivity() { // controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT // 使用系统原生方法检测暗色主题 - val isDarkTheme = resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val isDarkTheme = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES controller.isAppearanceLightStatusBars = isDarkTheme controller.isAppearanceLightNavigationBars = isDarkTheme - - setContent { - App() - } } } - -@Preview -@Composable -fun AppAndroidPreview() { - App() -} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/MainApplication.kt b/composeApp/src/androidMain/kotlin/com/taskttl/MainApplication.kt index 59046ef..f95b891 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/MainApplication.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/MainApplication.kt @@ -6,7 +6,6 @@ import android.app.Application import android.os.Bundle import com.google.firebase.FirebaseApp import com.taskttl.app.di.initKoin -import com.tencent.mmkv.MMKV import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -49,7 +48,6 @@ class MainApplication : Application() { override fun onActivityDestroyed(a: Activity) {} }) - MMKV.initialize(this@MainApplication) // 初始化 Firebase SDK FirebaseApp.initializeApp(this@MainApplication) // 初始化 Koin diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/app/di/AppModule.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/app/di/AppModule.android.kt index c8e76fe..a0c59be 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/app/di/AppModule.android.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/app/di/AppModule.android.kt @@ -2,8 +2,14 @@ package com.taskttl.app.di import com.taskttl.core.database.TaskTTLDatabase import com.taskttl.core.database.getDatabaseBuilder +import com.taskttl.domain.repository.AuthRepository +import com.taskttl.domain.repository.AuthRepositoryImpl +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind import org.koin.dsl.module actual val serviceModule = module { single { getDatabaseBuilder() } + + singleOf(::AuthRepositoryImpl).bind(AuthRepository::class) } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/common/DevTTLWebView.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/common/DevTTLWebView.android.kt index 849b1c1..1accf4a 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/common/DevTTLWebView.android.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/common/DevTTLWebView.android.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import android.view.ViewGroup import android.webkit.WebResourceError import android.webkit.WebResourceRequest +import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.layout.Box @@ -38,7 +39,7 @@ import taskttl.composeapp.generated.resources.webview_loading_error @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("SetJavaScriptEnabled") @Composable -actual fun DevTTLWebView(modifier: Modifier, url: String) { +actual fun DevTTLWebView(modifier: Modifier, url: String, enableJavaScript: Boolean) { // 状态管理 var isLoading by remember { mutableStateOf(true) } var hasError by remember { mutableStateOf(false) } @@ -47,8 +48,7 @@ actual fun DevTTLWebView(modifier: Modifier, url: String) { Box( - modifier = Modifier - .fillMaxSize() + modifier = Modifier.fillMaxSize() ) { val loadingError = stringResource(Res.string.webview_loading_error) // 使用AndroidView加载WebView @@ -62,15 +62,14 @@ actual fun DevTTLWebView(modifier: Modifier, url: String) { // 配置WebView设置 settings.apply { - javaScriptEnabled = true + javaScriptEnabled = enableJavaScript domStorageEnabled = true loadWithOverviewMode = true useWideViewPort = true // 启用缓存 - cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + cacheMode = WebSettings.LOAD_DEFAULT // 启用混合内容(HTTP和HTTPS) - mixedContentMode = - android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE + mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE } @@ -78,17 +77,14 @@ actual fun DevTTLWebView(modifier: Modifier, url: String) { override fun onPageStarted( view: WebView?, url: String?, - favicon: Bitmap? + favicon: Bitmap?, ) { super.onPageStarted(view, url, favicon) isLoading = true hasError = false } - override fun onPageFinished( - view: WebView?, - url: String? - ) { + override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) isLoading = false } @@ -97,7 +93,7 @@ actual fun DevTTLWebView(modifier: Modifier, url: String) { override fun onReceivedError( view: WebView?, request: WebResourceRequest?, - error: WebResourceError? + error: WebResourceError?, ) { super.onReceivedError(view, request, error) if (request?.isForMainFrame == true) { diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/notification/AppNotificationManager.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/notification/AppNotificationManager.android.kt index 3e5d757..5a36911 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/notification/AppNotificationManager.android.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/notification/AppNotificationManager.android.kt @@ -1,14 +1,19 @@ package com.taskttl.core.notification +import android.Manifest import android.annotation.SuppressLint import android.app.AlarmManager +import android.app.Notification.VISIBILITY_PRIVATE +import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.media.RingtoneManager import android.os.Build +import androidx.annotation.RequiresPermission import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder @@ -16,7 +21,7 @@ import androidx.work.WorkManager import androidx.work.workDataOf import com.taskttl.MainApplication import com.taskttl.R -import com.taskttl.core.alarm.AlarmReceiver +import com.taskttl.core.receiver.AlarmReceiver import com.taskttl.core.utils.LogUtils import com.taskttl.data.constant.Constant import java.util.concurrent.TimeUnit @@ -25,8 +30,8 @@ import kotlin.math.max @SuppressLint("StaticFieldLeak") actual object AppNotificationManager { private val context: Context = MainApplication.instance.applicationContext - private val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val notificationManager = NotificationManagerCompat.from(context) + // context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private const val ONE_DAY_MS = 24 * 60 * 60 * 1000L @@ -38,7 +43,7 @@ actual object AppNotificationManager { private fun setupNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (notificationManager.getNotificationChannel(Constant.CHANNEL_ID) == null) { - val channel = android.app.NotificationChannel( + val channel = NotificationChannel( Constant.CHANNEL_ID, "TaskTTL Notifications", NotificationManager.IMPORTANCE_HIGH @@ -53,6 +58,7 @@ actual object AppNotificationManager { max(0, triggerTimeMillis - System.currentTimeMillis()) /** 调度通知,安全可后台 */ + @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) actual suspend fun scheduleNotification(payload: NotificationPayload) { val delay = payload.delayMillis() @@ -78,7 +84,12 @@ actual object AppNotificationManager { } - /** AlarmManager 精确闹钟 */ + /** + * AlarmManager 精确闹钟 + * + * @param [context] 上下文 + * @param [payload] 有效载荷 + */ fun scheduleExactAlarm(context: Context, payload: NotificationPayload) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { @@ -99,7 +110,13 @@ actual object AppNotificationManager { LogUtils.d("AlarmDebug", "Alarm set for id=${payload.id} time=${payload.triggerTimeMillis}") } - /** 创建 Alarm PendingIntent */ + /** + * 创建 Alarm PendingIntent + * + * @param [context] 上下文 + * @param [payload] 有效载荷 + * @return [PendingIntent] + */ private fun createAlarmPendingIntent( context: Context, payload: NotificationPayload, @@ -118,7 +135,12 @@ actual object AppNotificationManager { ) } - /** WorkManager 长期/周期通知 */ + /** + * WorkManager 长期/周期通知 + * + * @param [context] 上下文 + * @param [payload] 有效载荷 + */ private fun scheduleWorkManager(context: Context, payload: NotificationPayload) { val workManager = WorkManager.getInstance(context) val data = workDataOf( @@ -157,20 +179,36 @@ actual object AppNotificationManager { } } - /** 立即显示通知 */ + /** + * 立即显示通知 + * 显示即时通知 + * @param [context] 上下文 + * @param [payload] 有效载荷 + */ + @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) fun showImmediateNotification(context: Context, payload: NotificationPayload) { val notification = NotificationCompat.Builder(context, Constant.CHANNEL_ID) .setContentTitle(payload.title) .setContentText(payload.message) - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.mipmap.ic_launcher_round) .setPriority(NotificationCompat.PRIORITY_HIGH) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) // 隐私锁屏通知 + .setPublicVersion( + NotificationCompat.Builder(context, Constant.CHANNEL_ID) + .setContentTitle(payload.title) + .setContentText(payload.message) + .setSmallIcon(R.mipmap.ic_launcher_round).build() + ) .setAutoCancel(true) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .build() notificationManager.notify(payload.id.hashCode(), notification) } - /** 取消通知 */ + /** + * 取消通知 + * @param [id] id + */ actual suspend fun cancelNotification(id: String) { val workManager = WorkManager.getInstance(context) workManager.cancelAllWorkByTag(id) @@ -186,7 +224,9 @@ actual object AppNotificationManager { notificationManager.cancel(id.hashCode()) } - /** 取消所有通知 */ + /** + * 取消所有通知 + */ actual suspend fun cancelAll() { WorkManager.getInstance(context).cancelAllWork() notificationManager.cancelAll() diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/alarm/AlarmReceiver.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/receiver/AlarmReceiver.kt similarity index 97% rename from composeApp/src/androidMain/kotlin/com/taskttl/core/alarm/AlarmReceiver.kt rename to composeApp/src/androidMain/kotlin/com/taskttl/core/receiver/AlarmReceiver.kt index 4eac940..12d4171 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/alarm/AlarmReceiver.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/receiver/AlarmReceiver.kt @@ -1,4 +1,4 @@ -package com.taskttl.core.alarm +package com.taskttl.core.receiver import android.content.BroadcastReceiver import android.content.Context diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/alarm/BootReceiver.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/receiver/BootReceiver.kt similarity index 97% rename from composeApp/src/androidMain/kotlin/com/taskttl/core/alarm/BootReceiver.kt rename to composeApp/src/androidMain/kotlin/com/taskttl/core/receiver/BootReceiver.kt index 4c19b7e..a2b15b1 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/alarm/BootReceiver.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/receiver/BootReceiver.kt @@ -1,4 +1,4 @@ -package com.taskttl.core.alarm +package com.taskttl.core.receiver import android.content.BroadcastReceiver import android.content.Context 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 6b1f862..13a26b6 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 @@ -2,18 +2,18 @@ package com.taskttl.core.utils import android.annotation.SuppressLint import android.content.Context -import android.content.res.Configuration import android.os.Build +import android.provider.Settings.Secure.ANDROID_ID +import android.provider.Settings.Secure.getString import android.telephony.TelephonyManager import com.google.android.gms.ads.identifier.AdvertisingIdClient import com.taskttl.BuildConfig import com.taskttl.MainApplication import com.taskttl.core.domain.BaseReq +import com.taskttl.data.constant.Constant import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import java.security.MessageDigest import java.util.UUID actual object DeviceUtils { @@ -21,43 +21,43 @@ actual object DeviceUtils { private val appContext: Context get() = MainApplication.instance.applicationContext - private const val PREF_UNIQUE_ID = "PREF_UNIQUE_ID" - // 请求超时时间 - private const val REQUEST_TIMEOUT_MS = 5_000L + private const val REQUEST_TIMEOUT_MS = 2_000L actual suspend fun getUniqueId(): String { // 首先检查是否已经生成过唯一ID - val savedId = StorageUtils.getString(PREF_UNIQUE_ID) + val savedId = StorageUtils.getString(Constant.PREF_UNIQUE_ID) // 如果已有保存的ID,直接返回 - if (savedId.isNotBlank()) { - return savedId - } + if (savedId.isNotBlank()) return savedId // 检查网络状态,如果没有网络直接生成本地ID // if (!isNetworkAvailable()) { // val localId = generateAppInstanceId() - // MMKVUtils.saveString(PREF_UNIQUE_ID, localId) + // MMKVUtils.saveString(Constant.PREF_UNIQUE_ID, localId) // return localId // } - // 尝试获取Google广告ID - val adId = try { - getGoogleAdId() - } catch (e: Exception) { - null - } + // 1) GAID/AAID + val gaId = getGaIdSafe() + if (isValidAdId(gaId)) return persistAndReturn(gaId!!) - val uniqueId = if (isValidAdId(adId)) { - adId.toString() - } else { - // 如果广告ID无效,生成应用实例ID - generateAppInstanceId() - } + // 2) ANDROID_ID + val ssaid = getAndroidIdSafe() + if (!ssaid.isNullOrBlank()) return persistAndReturn(ssaid) - // 保存生成的ID - StorageUtils.saveString(PREF_UNIQUE_ID, uniqueId) + // 3) UUID + val uuid = UUID.randomUUID().toString().replace("-", "") + return persistAndReturn(uuid) + } + + /** + * 持久化并返回 + * @param [uniqueId] 唯一ID + * @return [String] + */ + private fun persistAndReturn(uniqueId: String): String { + StorageUtils.saveString(Constant.PREF_UNIQUE_ID, uniqueId) return uniqueId } @@ -73,72 +73,50 @@ actual object DeviceUtils { ) } + /** + * 获取谷歌广告id + * @return [String?] + */ @SuppressLint("AdvertisingIdPolicy") - private suspend fun getGoogleAdId(): String? { - return withContext(Dispatchers.IO) { - try { - withTimeout(REQUEST_TIMEOUT_MS) { - val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(appContext) - if (adInfo.isLimitAdTrackingEnabled) null else adInfo.id - } - } catch (e: TimeoutCancellationException) { - null - } catch (e: Exception) { - null + private suspend fun getGaIdSafe(): String? = withContext(Dispatchers.IO) { + try { + withTimeout(REQUEST_TIMEOUT_MS) { + val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(appContext) + if (adInfo.isLimitAdTrackingEnabled) null else adInfo.id } + } catch (_: Throwable) { + null } } + /** * 验证是否是有效广告id * @param [adId] 广告id * @return [Boolean] */ private fun isValidAdId(adId: String?): Boolean { - return !adId.isNullOrBlank() && !adId.matches(Regex("^0+$")) && adId != "00000000-0000-0000-0000-000000000000" + if (adId.isNullOrBlank()) return false + if (adId == "00000000-0000-0000-0000-000000000000") return false + if (adId.matches(Regex("^0+$"))) return false + return true } - private fun generateAppInstanceId(): String { - val deviceIdentifiers = mutableListOf() - // 设备硬件信息 - deviceIdentifiers.add(buildDeviceInfo()) - return hashString(deviceIdentifiers.joinToString("|")) - } - - private fun buildDeviceInfo(): String { - return StringBuilder().apply { - append(Build.BOARD).append(":") - append(Build.BRAND).append(":") - append(Build.DEVICE).append(":") - append(Build.HARDWARE).append(":") - append(Build.MANUFACTURER).append(":") - append(Build.MODEL).append(":") - append(Build.PRODUCT) - }.toString() - } - - private fun hashString(input: String): String { + /** + * 获取安卓ID + * @return [String] + */ + @SuppressLint("HardwareIds") + private fun getAndroidIdSafe(): String? { + // 优先 ANDROID_ID(O+ 已是 app-scoped 且稳定) return try { - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) - // 返回前64字符 - bytesToHex(hash).take(64) - } catch (e: Exception) { - UUID.randomUUID().toString().replace("-", "").take(64) + val androidId = getString(appContext.contentResolver, ANDROID_ID) + if (androidId.isNullOrBlank() || androidId == "9774d56d682e549c") null else androidId + } catch (_: Throwable) { + null } } - private fun bytesToHex(bytes: ByteArray): String { - val hexChars = "0123456789abcdef" - return StringBuilder(bytes.size * 2).apply { - bytes.forEach { byte -> - val i = byte.toInt() and 0xff - append(hexChars[i shr 4]) - append(hexChars[i and 0x0f]) - } - }.toString() - } - fun getSimOrNetworkCountry(): String { val tm = appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager return when { diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/StorageUtils.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/StorageUtils.android.kt deleted file mode 100644 index 8e29265..0000000 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/StorageUtils.android.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.taskttl.core.utils - -import com.tencent.mmkv.MMKV -import kotlinx.serialization.json.Json - -actual object StorageUtils { - - val mmkv: MMKV - get() = MMKV.defaultMMKV() - - val json = Json { - ignoreUnknownKeys = true - isLenient = true - prettyPrint = true - } - - actual fun saveString(key: String, value: String) { - mmkv.encode(key, value) - } - - actual fun getString(key: String, defaultValue: String): String { - val value = mmkv.decodeString(key) - if (value.isNullOrEmpty()) { - mmkv.encode(key, defaultValue) - return defaultValue - } - return value - } - - actual fun saveInt(key: String, value: Int) { - mmkv.encode(key, value) - } - - actual fun getInt(key: String, defaultValue: Int): Int { - return mmkv.decodeInt(key, defaultValue) - } - - actual fun saveLong(key: String, value: Long) { - mmkv.encode(key, value) - } - - actual fun getLong(key: String, defaultValue: Long): Long { - return mmkv.decodeLong(key, defaultValue) - } - - actual fun saveBoolean(key: String, value: Boolean) { - mmkv.encode(key, value) - } - - actual fun getBoolean(key: String, defaultValue: Boolean): Boolean { - return mmkv.decodeBool(key, defaultValue) - } - - actual inline fun saveObject(key: String, value: T) { - val data = json.encodeToString(value) - mmkv.encode(key, data) - } - - actual inline fun getObject(key: String): T? { - val data = mmkv.decodeString(key) ?: return null - return try { - json.decodeFromString(data) - } catch (e: Exception) { - null - } - } - - actual fun contains(key: String): Boolean { - return mmkv.contains(key) - } - - actual fun remove(key: String) { - mmkv.removeValueForKey(key) - } - - actual fun clear() { - mmkv.clearAll() - } - -} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepository.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepository.android.kt deleted file mode 100644 index a4fc249..0000000 --- a/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepository.android.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.taskttl.domain.repository - -import android.credentials.GetCredentialException -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import com.facebook.login.LoginManager -import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException -import com.taskttl.MainApplication -import com.taskttl.core.utils.JsonUtils -import com.taskttl.core.utils.LogUtils -import com.taskttl.data.source.remote.dto.response.AuthResult -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import kotlin.coroutines.resume - -actual class AuthRepository { - - private val WEB_CLIENT_ID = - "649192447921-rtn0jklurc7cr4oalh9gh3684mnlklce.apps.googleusercontent.com" - - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - actual suspend fun loginWithGoogle(): AuthResult = withContext(Dispatchers.Main) { - val activity = MainApplication.currentActivity() - ?: return@withContext AuthResult.Error("未找到当前活动实例") - - return@withContext try { - val credentialManager = CredentialManager.create(activity) - - val signInWithGoogleOption = - GetSignInWithGoogleOption.Builder(WEB_CLIENT_ID).build() - - val request = GetCredentialRequest.Builder() - .addCredentialOption(signInWithGoogleOption) - .build() - - - val response = credentialManager.getCredential(context = activity, request = request) - - handleSignInWithGoogleOption(response) - - // 提取 token 信息 - val googleIdCredential = response.credential - val token = googleIdCredential.data.getString("idToken") ?: "" - val userId = googleIdCredential.data.getString("id") ?: "" - - AuthResult.Success(userId = userId, token = token) - } catch (e: GetCredentialException) { - e.message?.let { LogUtils.e("DevTTL", it) } - AuthResult.Error("获取凭据失败: ${e.message}") - } catch (e: Exception) { - e.message?.let { LogUtils.e("DevTTL", it) } - AuthResult.Error("Google 登录失败: ${e.message}") - } - } - - actual suspend fun loginWithFacebook(): AuthResult = suspendCancellableCoroutine { cont -> - val activity = MainApplication.currentActivity() ?: return@suspendCancellableCoroutine - try { - LoginManager.getInstance().logInWithReadPermissions(activity, listOf("email")) - // TODO: handle Facebook callback - cont.resume(AuthResult.Success("demo_user", "facebook_token")) - } catch (e: Exception) { - cont.resume(AuthResult.Error(e.message ?: "Facebook 登录失败")) - } - } - - /** - * 使用谷歌选项处理登录 - * @param [result] 结果 - */ - fun handleSignInWithGoogleOption(result: GetCredentialResponse) { - val credential = result.credential - - when (credential) { - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - val googleIdTokenCredential = - GoogleIdTokenCredential.createFrom(credential.data) - LogUtils.w( - "DevTTL", - JsonUtils.default.encodeToString(googleIdTokenCredential) - ) - } catch (e: GoogleIdTokenParsingException) { - LogUtils.e("DevTTL", "Received an invalid google id token response", e) - } - } else { - // Catch any unrecognized credential type here. - LogUtils.e("DevTTL", "Unexpected type of credential") - } - } - - else -> { - // Catch any unrecognized credential type here. - LogUtils.e("DevTTL", "Unexpected type of credential") - } - } - } -} \ 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 new file mode 100644 index 0000000..8983421 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt @@ -0,0 +1,92 @@ +package com.taskttl.domain.repository + +import android.credentials.GetCredentialException +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import com.facebook.login.LoginManager +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.taskttl.MainApplication +import com.taskttl.core.utils.LogUtils +import com.taskttl.data.constant.Constant +import com.taskttl.data.constant.ProviderEnum +import com.taskttl.data.source.remote.dto.response.Account +import com.taskttl.data.source.remote.dto.response.AuthResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume + +/** + * 身份验证存储库impl + * @author DevTTL + * @date 2025/10/30 + * @constructor 创建[AuthRepositoryImpl] + */ +class AuthRepositoryImpl() : AuthRepository { + + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + override suspend fun loginWithGoogle(): AuthResult = withContext(Dispatchers.Main) { + val activity = MainApplication.currentActivity() + ?: return@withContext AuthResult.Error("Context is null") + + val googleIdOption = GetSignInWithGoogleOption + .Builder(Constant.WEB_CLIENT_ID) + .build() + + val request: GetCredentialRequest = GetCredentialRequest + .Builder() + .addCredentialOption(googleIdOption) + .build() + + val credentialManager = CredentialManager.create(activity) + + return@withContext try { + val result = credentialManager.getCredential(context = activity, request = request) + val credential = result.credential + + val isGoogleCred = + credential is CustomCredential && + credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL + + if (isGoogleCred) { + val tokenResult = GoogleIdTokenCredential.createFrom(credential.data) + val account = + Account( + id = tokenResult.id, + idToken = tokenResult.idToken, + provider = ProviderEnum.GOOGLE + ) + AuthResult.Success(account) + } else { + AuthResult.Canceled + } + } catch (e: GetCredentialException) { + e.message?.let { LogUtils.e("DevTTL", it) } + AuthResult.Error("获取凭据失败: ${e.message}") + } catch (e: Exception) { + e.message?.let { LogUtils.e("DevTTL", it) } + AuthResult.Error("Google 登录失败: ${e.message}") + } + } + + override suspend fun loginWithFacebook(): AuthResult = suspendCancellableCoroutine { cont -> + val activity = MainApplication.currentActivity() ?: return@suspendCancellableCoroutine + try { + LoginManager.getInstance().logInWithReadPermissions(activity, listOf("email")) + // TODO: handle Facebook callback + val account = Account( + id = "", + idToken = "demo_user", + provider = ProviderEnum.FACEBOOK, + ) + cont.resume(AuthResult.Success(account)) + } catch (e: Exception) { + cont.resume(AuthResult.Error(e.message ?: "Facebook 登录失败")) + } + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/presentation/common/foundation/ImageLoaderProvider.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/presentation/common/foundation/ImageLoaderProvider.android.kt new file mode 100644 index 0000000..72f10a3 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/taskttl/presentation/common/foundation/ImageLoaderProvider.android.kt @@ -0,0 +1,16 @@ +package com.taskttl.presentation.common.foundation +// 获取磁盘缓存路径 +import android.content.Context +import com.taskttl.MainApplication +import okio.Path +import okio.Path.Companion.toOkioPath +import java.io.File + +/** + * 获取磁盘缓存路径 + * @return [Path?] + */ +actual fun provideDiskCachePath(): Path? { + val context: Context = MainApplication.instance.applicationContext + return File(context.cacheDir, "coil_disk_cache").toOkioPath() +} \ No newline at end of file diff --git a/composeApp/src/androidMain/res/values-v31/styles.xml b/composeApp/src/androidMain/res/values-v31/styles.xml new file mode 100644 index 0000000..ccbbc4e --- /dev/null +++ b/composeApp/src/androidMain/res/values-v31/styles.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/values/styles.xml b/composeApp/src/androidMain/res/values/styles.xml new file mode 100644 index 0000000..0d9e288 --- /dev/null +++ b/composeApp/src/androidMain/res/values/styles.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt index 0288715..5039f33 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt @@ -6,17 +6,17 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel /** * 应用 */ -@Preview @Composable fun App(viewModel: AppViewModel = koinViewModel()) { + GlobalImageLoader() val state by viewModel.state.collectAsState() diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/AppState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/AppState.kt index fa74f8f..e5a9ba6 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/AppState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/AppState.kt @@ -3,6 +3,16 @@ package com.taskttl.app import com.taskttl.core.base.BaseState import com.taskttl.core.manager.ThemeMode +/** + * 应用程序状态 + * @author DevTTL + * @date 2025/10/30 + * @constructor 创建[AppState] + * @param [isLoading] 正在加载 + * @param [isProcessing] 正在处理 + * @param [error] 错误 + * @param [themeMode] 主题模式 + */ data class AppState( override val isLoading: Boolean = false, override val isProcessing: Boolean = false, @@ -18,6 +28,13 @@ data class AppState( */ sealed class AppIntent { + /** + * 加载应用 + * @author DevTTL + * @date 2025/11/07 + */ + object LoadApp: AppIntent() + /** * 加载主题 * @author DevTTL @@ -26,4 +43,10 @@ sealed class AppIntent { object LoadTheme : AppIntent() } +/** + * app效果 + * @author DevTTL + * @date 2025/10/30 + * @constructor 创建[AppEffect] + */ sealed class AppEffect {} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/AppViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/AppViewModel.kt index 9e83c56..2be713d 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/AppViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/AppViewModel.kt @@ -1,8 +1,14 @@ package com.taskttl.app +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel +import com.taskttl.core.domain.constant.PointEvent import com.taskttl.core.manager.ThemeManager +import com.taskttl.core.utils.StorageUtils +import com.taskttl.data.constant.Constant +import com.taskttl.data.source.remote.api.TaskTTLApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch /** @@ -12,6 +18,7 @@ import kotlinx.coroutines.launch * @constructor 创建[AppViewModel] * @param [themeManager] 主题管理器 */ +@Stable class AppViewModel(private val themeManager: ThemeManager) : BaseViewModel(AppState()) { @@ -19,15 +26,28 @@ class AppViewModel(private val themeManager: ThemeManager) : viewModelScope.launch { themeManager.themeMode.collect { mode -> updateState { copy(themeMode = mode) } } } - // handleIntent(AppIntent.LoadTheme) + handleIntent(AppIntent.LoadApp) } public override fun handleIntent(intent: AppIntent) { when (intent) { + is AppIntent.LoadApp -> loadApp() is AppIntent.LoadTheme -> loadTheme() } } + /** + * 加载应用 + */ + private fun loadApp() { + viewModelScope.launch(Dispatchers.Default) { + val uniqueId = StorageUtils.getString(Constant.PREF_UNIQUE_ID) + if (!uniqueId.isNotBlank()) { + TaskTTLApi.postPoint(PointEvent.AppLaunch) + } + } + } + private fun loadTheme() { viewModelScope.launch { updateState { copy(themeMode = themeManager.themeMode.value) } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/DataModels.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/DataModels.kt index f135a41..25a5074 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/DataModels.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/DataModels.kt @@ -9,11 +9,11 @@ import com.taskttl.data.mapper.TaskMapper import com.taskttl.data.repository.CategoryRepositoryImpl import com.taskttl.data.repository.CountdownRepositoryImpl import com.taskttl.data.repository.TaskRepositoryImpl -import com.taskttl.domain.repository.AuthRepository import com.taskttl.domain.repository.CategoryRepository import com.taskttl.domain.repository.CountdownRepository import com.taskttl.domain.repository.TaskRepository import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind import org.koin.dsl.module @@ -29,9 +29,7 @@ val dataModule = module { single { CountdownMapper() } single { CategoryMapper() } - single { TaskRepositoryImpl(get(), get()) } - single { CountdownRepositoryImpl(get(), get()) } - single { CategoryRepositoryImpl(get(), get(), get(), get()) } - - singleOf(::AuthRepository) -} \ No newline at end of file + singleOf(::TaskRepositoryImpl).bind(TaskRepository::class) + singleOf(::CountdownRepositoryImpl).bind(CountdownRepository::class) + singleOf(::CategoryRepositoryImpl).bind(CategoryRepository::class) +} 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 be6ca5e..da52387 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt @@ -28,7 +28,9 @@ import org.koin.dsl.module */ fun initKoin(config: (KoinApplication.() -> Unit)? = null) { startKoin { + // 先执行用户自定义配置 config?.invoke(this) + // 再加载模块 modules(repositoryModule, viewModelModule, serviceModule, dataModule) } } @@ -52,4 +54,4 @@ val viewModelModule = module { viewModelOf(::SettingsViewModel) viewModelOf(::FeedbackViewModel) viewModelOf(::AuthViewModel) -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/common/DevTTLWebView.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/common/DevTTLWebView.kt index 11e068c..bd6bc40 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/common/DevTTLWebView.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/common/DevTTLWebView.kt @@ -9,4 +9,4 @@ import androidx.compose.ui.Modifier * @param [url] 网址 */ @Composable -expect fun DevTTLWebView(modifier: Modifier, url: String) \ No newline at end of file +expect fun DevTTLWebView(modifier: Modifier, url: String,enableJavaScript: Boolean = true) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/manager/ThemeManager.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/manager/ThemeManager.kt index 6eb32cd..1c77f1c 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/manager/ThemeManager.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/manager/ThemeManager.kt @@ -27,7 +27,6 @@ class ThemeManager() { private fun loadThemeFromPreferences() { val modeString = StorageUtils.getString(Constant.KEY_DARK_MODE, ThemeMode.SYSTEM.name) _themeMode.value = ThemeMode.fromName(modeString) - } /** 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 666fdaa..dc4582a 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt @@ -10,6 +10,12 @@ 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 LOGIN_URL = "$BASE_URL/login" + + /** 三方登录地址 */ + const val THIRD_PARTY_LOGIN_URL = "$BASE_URL/thirdPartyLogin" + /** 反馈地址 */ const val FEEDBACK_URL = "$BASE_URL/feedback" 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 65e4a25..6373cf4 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt @@ -1,17 +1,26 @@ 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 + + /** * 存储工具 * @author admin * @date 2025/08/11 */ -expect object StorageUtils { +object StorageUtils { + val settings: Settings = Settings() /** * 保存字符串值 * @param key 键 * @param value 值 */ - fun saveString(key: String, value: String) + fun saveString(key: String, value: String) { + settings.putString(key,value) + } /** * 获取字符串值 @@ -19,14 +28,18 @@ expect object StorageUtils { * @param defaultValue 默认值 * @return 存储的字符串值或默认值 */ - fun getString(key: String, defaultValue: String = ""): String + fun getString(key: String, defaultValue: String = ""): String { + return settings.getString(key,defaultValue) + } /** * 保存整数值 * @param key 键 * @param value 值 */ - fun saveInt(key: String, value: Int) + fun saveInt(key: String, value: Int) { + settings.putInt(key,value) + } /** * 获取整数值 @@ -34,7 +47,9 @@ expect object StorageUtils { * @param defaultValue 默认值 * @return 存储的整数值或默认值 */ - fun getInt(key: String, defaultValue: Int = 0): Int + fun getInt(key: String, defaultValue: Int = 0): Int { + return settings.getInt(key,defaultValue) + } /** @@ -42,7 +57,9 @@ expect object StorageUtils { * @param [key] 键 * @param [value] 值 */ - fun saveLong(key: String, value: Long) + fun saveLong(key: String, value: Long) { + settings.putLong(key,value) + } /** * 获取长整数值 @@ -50,14 +67,18 @@ expect object StorageUtils { * @param defaultValue 默认值 * @return 存储的长整数值或默认值 */ - fun getLong(key: String, defaultValue: Long): Long + fun getLong(key: String, defaultValue: Long): Long { + return settings.getLong(key,defaultValue) + } /** * 保存布尔值 * @param key 键 * @param value 值 */ - fun saveBoolean(key: String, value: Boolean) + fun saveBoolean(key: String, value: Boolean) { + settings.putBoolean(key,value) + } /** * 获取布尔值 @@ -65,37 +86,49 @@ expect object StorageUtils { * @param defaultValue 默认值 * @return 存储的布尔值或默认值 */ - fun getBoolean(key: String, defaultValue: Boolean = false): Boolean + fun getBoolean(key: String, defaultValue: Boolean = false): Boolean { + return settings.getBoolean(key,defaultValue) + } /** * 保存对象 * @param key 键 * @param value 对象 */ - inline fun saveObject(key: String, value: T) + inline fun saveObject(key: String, value: T) { + settings[key] = value + } /** * 获取对象 * @param key 键 * @return 存储的对象或null */ - inline fun getObject(key: String): T? + inline fun getObject(key: String): T? { + return settings[key] + } /** * 包含 * @param [key] 钥匙 * @return [Boolean] */ - fun contains(key: String): Boolean + fun contains(key: String): Boolean { + return settings.contains(key) + } /** * 删除键值对 * @param key 键 */ - fun remove(key: String) + fun remove(key: String) { + settings.remove(key) + } /** * 清除所有数据 */ - fun clear() + fun clear() { + settings.clear() + } } \ No newline at end of file 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 9a3929e..22729ca 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt @@ -6,9 +6,15 @@ package com.taskttl.data.constant * @date 2025/10/24 */ object Constant { + /** 唯一ID */ + const val PREF_UNIQUE_ID = "PREF_UNIQUE_ID" + /** 频道id */ const val CHANNEL_ID = "taskttl_channel" /** 按键暗模式 */ const val KEY_DARK_MODE = "dark_mode" + + const val WEB_CLIENT_ID = + "649192447921-rtn0jklurc7cr4oalh9gh3684mnlklce.apps.googleusercontent.com" } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/ProviderEnum.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/ProviderEnum.kt new file mode 100644 index 0000000..e58ebb8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/ProviderEnum.kt @@ -0,0 +1,14 @@ +package com.taskttl.data.constant + +/** + * 提供者枚举 + * @author DevTTL + * @date 2025/11/13 + * @constructor 创建[ProviderEnum] + */ +enum class ProviderEnum { + GOOGLE, + FACEBOOK, + APPLE, + GITHUB +} \ No newline at end of file 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 238ba31..6eadde8 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 @@ -7,7 +7,10 @@ import com.taskttl.core.network.KtorClient import com.taskttl.core.utils.DeviceUtils import com.taskttl.core.utils.LogUtils 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.response.Account +import com.taskttl.data.source.remote.dto.response.AuthResult import com.taskttl.data.source.remote.dto.response.FeedbackResp import org.jetbrains.compose.resources.getString import taskttl.composeapp.generated.resources.Res @@ -15,6 +18,20 @@ import taskttl.composeapp.generated.resources.feedback_error object TaskTTLApi { + /** + * 第三方登录 + * @param [account] 账户 + * @return [FeedbackResp] + */ + suspend fun thirdPartyLogin(account: Account): FeedbackResp { + try { + val loginReq = LoginReq(account.id,account.idToken,account.provider.name) + return KtorClient.postJson(ApiConfig.THIRD_PARTY_LOGIN_URL,loginReq.toJson()) + } catch (_: Exception) { + throw Exception(getString(Res.string.feedback_error)) + } + } + /** * 发布反馈 * @param [feedbackReq] 反馈请求 @@ -25,7 +42,6 @@ object TaskTTLApi { feedbackReq.baseReq = DeviceUtils.getDeviceInfo() return KtorClient.postJson(ApiConfig.FEEDBACK_URL, feedbackReq.toJson()) } catch (e: Exception) { - LogUtils.e("DevTTL",e.message.toString()) 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 new file mode 100644 index 0000000..9eb19ec --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt @@ -0,0 +1,24 @@ +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/response/AuthResult.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt index 3e59170..c20f8aa 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 @@ -1,7 +1,26 @@ package com.taskttl.data.source.remote.dto.response +import com.taskttl.data.constant.ProviderEnum + sealed class AuthResult { - data class Success(val userId: String, val token: String) : AuthResult() - data class Error(val message: String) : AuthResult() + data class Success(val account: Account) : AuthResult() + data class Error(val message: String?) : AuthResult() data object Canceled : AuthResult() +} + +/** + * 账户 + * @author DevTTL + * @date 2025/11/13 + * @constructor 创建[Account] + * @param [id] id + * @param [idToken] id令牌 + * @param [provider] 提供者 + */ +data class Account( + val id: String, + val idToken: String, + val provider: ProviderEnum, +) { + val isEmpty: Boolean = idToken.isEmpty() || id.isEmpty() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/AuthRepository.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/AuthRepository.kt index a6caefe..2cbd304 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/AuthRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/AuthRepository.kt @@ -2,7 +2,23 @@ package com.taskttl.domain.repository import com.taskttl.data.source.remote.dto.response.AuthResult -expect class AuthRepository() { +/** + * 身份验证存储库 + * @author DevTTL + * @date 2025/10/30 + * @constructor 创建[AuthRepository] + */ +interface AuthRepository { + + /** + * 使用谷歌登录 + * @return [AuthResult] + */ suspend fun loginWithGoogle(): AuthResult + + /** + * 使用脸书登录 + * @return [AuthResult] + */ suspend fun loginWithFacebook(): AuthResult } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/AppNav.kt b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/AppNav.kt index 51940bb..a6b2a72 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/AppNav.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/AppNav.kt @@ -1,5 +1,5 @@ package com.taskttl.navigation - +// 应用导航 import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt index 941c269..30e4190 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt @@ -1,5 +1,5 @@ package com.taskttl.navigation - +// 主导航 import androidx.compose.foundation.background import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -88,7 +88,6 @@ fun MainNav() { navController = mainNavController, startDestination = Routes.Main.Task ) { - // 任务 composable { TaskScreen(navController = mainNavController) } @@ -114,7 +113,6 @@ fun MainNav() { ) } - // 倒数日 composable { CountdownScreen(navController = mainNavController) } @@ -147,11 +145,9 @@ fun MainNav() { StatisticsScreen(navController = mainNavController) } - // 设置界面 composable { SettingsScreen(navController = mainNavController) } - // 分类管理 composable { backStackEntry -> CategoryScreen( navController = mainNavController, @@ -159,14 +155,12 @@ fun MainNav() { onNavigateBack = { mainNavController.popBackStack() } ) } - // 添加分类 composable { CategoryEditScreen( categoryId = null, onNavigateBack = { mainNavController.popBackStack() } ) } - // 编辑分类 composable { backStackEntry -> val editCategory: Routes.Main.Settings.EditCategory = backStackEntry.toRoute() CategoryEditScreen( @@ -174,28 +168,23 @@ fun MainNav() { onNavigateBack = { mainNavController.popBackStack() } ) } - // 数据管理 composable { DataManagementScreen(onNavigateBack = { mainNavController.popBackStack() }) } - // 反馈页面 composable { FeedbackScreen(onNavigateBack = { mainNavController.popBackStack() }) } - // 隐私 composable { PrivacyScreen( onNavigateBack = { mainNavController.popBackStack() } ) } - // 关于页面 composable { AboutScreen( onNavigateBack = { mainNavController.popBackStack() } ) } - // 登录页面 composable { LoginScreen( 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 9055df6..784d6df 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt @@ -3,7 +3,7 @@ package com.taskttl.navigation import kotlinx.serialization.Serializable /** - * 路线 + * 路由 * @author DevTTL * @date 2025/06/25 * @constructor 创建[Routes] @@ -15,6 +15,11 @@ sealed interface Routes { data object Splash : Routes + /** + * 引导 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Onboarding : Routes @@ -22,62 +27,160 @@ sealed interface Routes { @Serializable data object Main : Routes { + /** + * 任务 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Task : Routes { + /** + * 添加任务 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object AddTask : Routes + /** + * 编辑任务 + * @author DevTTL + * @date 2025/11/07 + * @constructor 创建[EditTask] + * @param [taskId] 任务id + */ @Serializable data class EditTask(val taskId: String) : Routes + /** + * 任务详情 + * @author DevTTL + * @date 2025/11/07 + * @constructor 创建[TaskDetail] + * @param [taskId] 任务id + */ @Serializable data class TaskDetail(val taskId: String) : Routes } + /** + * 倒计时 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Countdown : Routes { + /** + * 添加倒计时 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object AddCountdown : Routes + /** + * 编辑倒计时 + * @author DevTTL + * @date 2025/11/07 + * @constructor 创建[EditCountdown] + * @param [countdownId] 倒计时id + */ @Serializable data class EditCountdown(val countdownId: String) : Routes + /** + * 倒计时详情 + * @author DevTTL + * @date 2025/11/07 + * @constructor 创建[CountdownDetail] + * @param [countdownId] 倒计时id + */ @Serializable data class CountdownDetail(val countdownId: String) : Routes } + /** + * 统计 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Statistics : Routes + /** + * 设置 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Settings : Routes { + /** + * 类别管理 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object CategoryManagement : Routes + /** + * 添加类别 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object AddCategory : Routes + /** + * 编辑类别 + * @author DevTTL + * @date 2025/11/07 + * @constructor 创建[EditCategory] + * @param [categoryId] 类别id + */ @Serializable data class EditCategory(val categoryId: String) : Routes + /** + * 数据管理 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object DataManagement : Routes + /** + * 反馈 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Feedback : Routes + /** + * 隐私 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Privacy : Routes + /** + * 关于 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object About : Routes + /** + * 登录 + * @author DevTTL + * @date 2025/11/07 + */ @Serializable data object Login : Routes } - - } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/LoadingOverlay.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/LoadingOverlay.kt index 606ee60..ddfaebb 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/LoadingOverlay.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/LoadingOverlay.kt @@ -37,10 +37,8 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.coroutines.delay import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.loading @@ -118,14 +116,3 @@ private fun RotatingGradientRing( ) } } - -@Preview -@Composable -private fun PreviewTaskTTLLoading() { - var show by remember { mutableStateOf(true) } - LaunchedEffect(Unit) { - delay(3000) - show = false - } - LoadingOverlay(visible = show) -} \ No newline at end of file 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 new file mode 100644 index 0000000..b20c6be --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt @@ -0,0 +1,37 @@ +package com.taskttl.presentation.common.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import coil3.compose.AsyncImage +import coil3.request.CachePolicy +import coil3.request.ImageRequest +import coil3.request.crossfade +import org.jetbrains.compose.resources.painterResource +import taskttl.composeapp.generated.resources.Res +import taskttl.composeapp.generated.resources.ic_launcher + +@Composable +fun NetworkImage( + url: String, + contentDescription: String? = null, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Crop, +) { + val context = coil3.compose.LocalPlatformContext.current + AsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context) + .data(url) + // 缓存策略:启用内存 + 磁盘缓存 + .memoryCachePolicy(CachePolicy.ENABLED) + .diskCachePolicy(CachePolicy.ENABLED) + // 可选:淡入动画 + .crossfade(true) + .build(), + contentDescription = contentDescription, + contentScale = contentScale, + placeholder = painterResource(Res.drawable.ic_launcher), + error = painterResource(Res.drawable.ic_launcher) + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000..ecf5c07 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt @@ -0,0 +1,50 @@ +package com.taskttl.presentation.common.foundation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.compose.LocalPlatformContext +import coil3.disk.DiskCache +import coil3.memory.MemoryCache +import coil3.request.crossfade +import okio.Path + +/** + * 全局图像加载器 + */ +@Composable +fun GlobalImageLoader() { + val platformContext = LocalPlatformContext.current + + remember(platformContext) { + val imageLoader = ImageLoader.Builder(platformContext) + // 内存缓存 + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(platformContext, 0.25) // 占总内存25% + .build() + } + // 磁盘缓存(如果当前平台支持) + .diskCache { + provideDiskCachePath()?.let { path -> + DiskCache.Builder() + .directory(path) + .maxSizePercent(0.02) + // .maxSizeBytes(128L * 1024 * 1024) // 128MB + .build() + } + } + // 淡入动画 + .crossfade(true) + .build() + // 设置为全局单例 + SingletonImageLoader.setSafe { imageLoader } + } +} + +/** + * 获取磁盘缓存路径 + * @return [Path?] + */ +expect fun provideDiskCachePath(): Path? \ 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/auth/AuthState.kt index b87bfd6..01f1878 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt @@ -1,6 +1,7 @@ package com.taskttl.presentation.features.auth import com.taskttl.core.base.BaseState +import com.taskttl.presentation.features.task.list.TaskEffect /** @@ -39,6 +40,10 @@ sealed class AuthIntent { * @date 2025/10/26 */ object LoginWithFacebook: AuthIntent() + + object ClearError: AuthIntent() + + object Logout : AuthIntent() } /** @@ -47,4 +52,13 @@ sealed class AuthIntent { * @date 2025/10/26 * @constructor 创建[AuthEffect] */ -sealed class AuthEffect {} \ No newline at end of file +sealed class AuthEffect { + /** + * 显示消息 + * @author admin + * @date 2025/09/27 + * @constructor 创建[ShowMessage] + * @param [message] 消息 + */ + data class ShowMessage(val message: String) : AuthEffect() +} \ 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 index 13d33bb..23b487b 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt @@ -1,7 +1,11 @@ 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 @@ -11,27 +15,76 @@ import kotlinx.coroutines.launch * @date 2025/10/26 * @constructor 创建[AuthViewModel] */ +@Stable class AuthViewModel(private val authRepository: AuthRepository) : BaseViewModel(AuthState()) { public override fun handleIntent(intent: AuthIntent) { when (intent) { - AuthIntent.LoginWithGoogle -> { - // TODO: 调用 Google 登录逻辑 - println("Google 登录触发") - viewModelScope.launch { - authRepository.loginWithGoogle() - } - } + is AuthIntent.LoginWithGoogle -> onLoginWithGoogle() - AuthIntent.LoginWithFacebook -> { - // TODO: 调用 Facebook 登录逻辑 - println("Facebook 登录触发") - viewModelScope.launch { - authRepository.loginWithFacebook() + 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/auth/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt index e2a9740..6550a0e 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt @@ -1,21 +1,14 @@ package com.taskttl.presentation.features.auth -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures 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 @@ -28,42 +21,76 @@ 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.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.Text 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.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput 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 kotlinx.coroutines.launch +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 +/** + * 登录屏幕 + * @param [onNavigateBack] 返回导航 + * @param [viewModel] 视图模型 + */ @OptIn(ExperimentalMaterial3Api::class, InternalResourceApi::class) @Composable fun LoginScreen( onNavigateBack: () -> Unit, viewModel: AuthViewModel = 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 -> {} + } + } + } + + state.error?.let { error -> + ErrorDialog( + errorMessage = state.error, + onDismiss = { viewModel.handleIntent(AuthIntent.ClearError) } + ) + } + Box( modifier = Modifier.fillMaxSize() ) { @@ -76,116 +103,61 @@ fun LoginScreen( onBackClick = { onNavigateBack.invoke() } ) - // --- 动画状态 --- - val logoAlpha = remember { Animatable(0f) } - val logoScale = rememberInfiniteTransition().animateFloat( - initialValue = 0.95f, - targetValue = 1.05f, - animationSpec = infiniteRepeatable( - animation = tween(2000, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ) - ) + Spacer(modifier = Modifier.height(48.dp)) - val cardOffset = remember { Animatable(200f) } - val cardAlpha = remember { Animatable(0f) } - - val googleButtonAlpha = remember { Animatable(0f) } - val facebookButtonAlpha = remember { Animatable(0f) } - val copyrightAlpha = remember { Animatable(0f) } - - // --- 启动动画顺序 --- - LaunchedEffect(Unit) { - // Logo 渐显 - logoAlpha.animateTo(1f, tween(500)) - // 卡片滑入 - launch { cardOffset.animateTo(0f, tween(700, easing = FastOutSlowInEasing)) } - launch { cardAlpha.animateTo(1f, tween(700)) } - // 按钮依次出现 - googleButtonAlpha.animateTo(1f, tween(400)) - facebookButtonAlpha.animateTo(1f, tween(400)) - // 版权淡入 - copyrightAlpha.animateTo(1f, tween(500)) - } Column( modifier = Modifier.fillMaxSize() .background(MaterialTheme.colorScheme.background) .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + horizontalAlignment = Alignment.CenterHorizontally ) { - // --- 登录卡片 --- - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp) - .graphicsLayer { - translationY = cardOffset.value - alpha = cardAlpha.value - }, - shape = RoundedCornerShape(24.dp), - elevation = CardDefaults.cardElevation(8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Logo - AsyncImage( - model = Res.getUri("drawable/ic_launcher.png"), - contentDescription = null, - modifier = Modifier - .size(120.dp) - .graphicsLayer { - alpha = logoAlpha.value - scaleX = logoScale.value - scaleY = logoScale.value - }, - contentScale = ContentScale.Fit - ) + // 应用 Logo + 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(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) - AnimatedLoginButton( - icon = Icons.Default.Language, - text = "使用 Google 登录", - backgroundColor = Color(0xFFDB4437), - alpha = googleButtonAlpha.value, - onClick = { viewModel.handleIntent(AuthIntent.LoginWithGoogle) } - ) + // Google 登录按钮 + LoginButton( + icon = Icons.Default.Language, + text = "使用 Google 登录", + backgroundColor = Color(0xFFDB4437), + onClick = { viewModel.handleIntent(AuthIntent.LoginWithGoogle) } + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - AnimatedLoginButton( - icon = Icons.Default.Facebook, - text = "使用 Facebook 登录", - backgroundColor = Color(0xFF1877F2), - alpha = facebookButtonAlpha.value, - onClick = { viewModel.handleIntent(AuthIntent.LoginWithFacebook) } - ) - } - } + // Facebook 登录按钮 + LoginButton( + icon = Icons.Default.Facebook, + text = "使用 Facebook 登录", + backgroundColor = Color(0xFF1877F2), + onClick = { viewModel.handleIntent(AuthIntent.LoginWithFacebook) } + ) + + Spacer(modifier = Modifier.weight(1f)) Text( text = "© DevTTL Team. All rights reserved.", @@ -194,42 +166,28 @@ fun LoginScreen( modifier = Modifier.padding(bottom = 8.dp) ) } + } } } @Composable -private fun AnimatedLoginButton( +private fun LoginButton( icon: ImageVector, text: String, backgroundColor: Color, - alpha: Float = 1f, onClick: () -> Unit, ) { - var pressed by remember { mutableStateOf(false) } - val scale by animateFloatAsState(if (pressed) 0.95f else 1f, tween(100)) Row( modifier = Modifier .fillMaxWidth() .height(52.dp) - .graphicsLayer { scaleX = scale; scaleY = scale; this.alpha = alpha } .background(color = backgroundColor, shape = RoundedCornerShape(12.dp)) .clickable( - onClick = onClick, - onClickLabel = text, interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current - ) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - pressed = true - tryAwaitRelease() - pressed = false - } - ) - } + indication = null + ) { onClick() } .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center @@ -250,3 +208,38 @@ private fun AnimatedLoginButton( ) } } + +@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/category/list/CategoryViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryViewModel.kt index 28a828b..9a09bce 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryViewModel.kt @@ -1,5 +1,6 @@ package com.taskttl.presentation.features.category.list +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel import com.taskttl.domain.model.Category @@ -28,6 +29,7 @@ import taskttl.composeapp.generated.resources.category_update_success * @constructor 创建[CategoryViewModel] * @param [categoryRepository] 类别存储库 */ +@Stable class CategoryViewModel(private val categoryRepository: CategoryRepository) : BaseViewModel(CategoryState()) { 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 b30f70d..2c1a5f8 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 @@ -64,7 +64,6 @@ import com.taskttl.presentation.common.components.CategoryFilter import com.taskttl.presentation.common.components.ErrorDialog import com.taskttl.presentation.common.components.LoadingOverlay import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.delete @@ -77,7 +76,6 @@ import taskttl.composeapp.generated.resources.text_no_countdowns import taskttl.composeapp.generated.resources.title_countdown @Composable -@Preview fun CountdownScreen( navController: NavHostController, viewModel: CountdownViewModel = koinViewModel(), diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownViewModel.kt index 2087ac4..c482515 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownViewModel.kt @@ -1,5 +1,6 @@ package com.taskttl.presentation.features.countdown.list +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel import com.taskttl.domain.model.Category @@ -26,6 +27,7 @@ import taskttl.composeapp.generated.resources.countdown_update_success * @constructor 创建[CountdownViewModel] * @param [countdownRepository] 倒计时存储库 */ +@Stable class CountdownViewModel( private val countdownRepository: CountdownRepository, private val categoryRepository: CategoryRepository, 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 2d709ea..5a00b2e 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 @@ -37,7 +37,6 @@ import com.taskttl.domain.model.OnboardingPage import com.taskttl.navigation.Routes import kotlinx.coroutines.flow.collectLatest import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.continue_text @@ -49,7 +48,6 @@ import taskttl.composeapp.generated.resources.skip_text * @param [navigatorToRoute] 导航到路线 * @param [viewModel] 视图模型 */ -@Preview @Composable fun OnboardingScreen( navigatorToRoute: (Routes) -> Unit, 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 554e301..ae44f08 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 @@ -23,7 +23,25 @@ data class OnboardingState( * @constructor 创建[OnboardingIntent] */ sealed class OnboardingIntent { + /** + * 初始化 + * @author DevTTL + * @date 2025/11/07 + */ + object Initialization: OnboardingIntent() + + /** + * 下一页 + * @author DevTTL + * @date 2025/11/07 + */ object NextPage : OnboardingIntent() + + /** + * 标记引导完成 + * @author DevTTL + * @date 2025/11/07 + */ object MarkOnboardingCompleted : OnboardingIntent() } 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 93498fa..1ca4a49 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 @@ -1,9 +1,16 @@ package com.taskttl.presentation.features.onboarding +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel +import com.taskttl.core.domain.constant.PointEvent +import com.taskttl.core.utils.DeviceUtils +import com.taskttl.core.utils.LogUtils +import com.taskttl.core.utils.StorageUtils +import com.taskttl.data.source.remote.api.TaskTTLApi import com.taskttl.domain.repository.CategoryRepository import com.taskttl.domain.repository.OnboardingRepository +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch /** @@ -14,18 +21,37 @@ import kotlinx.coroutines.launch * @param [onboardingRepository] 引导存储库 * @param [categoryRepository] 类别存储库 */ +@Stable class OnboardingViewModel( private val onboardingRepository: OnboardingRepository, private val categoryRepository: CategoryRepository, ) : BaseViewModel(initialState = OnboardingState()) { + init { + processIntent(OnboardingIntent.Initialization) + } + override fun handleIntent(intent: OnboardingIntent) { when (intent) { + is OnboardingIntent.Initialization -> initialization() is OnboardingIntent.NextPage -> nextPage() is OnboardingIntent.MarkOnboardingCompleted -> markOnboardingCompleted() } } + private fun initialization() { + viewModelScope.launch(Dispatchers.Default) { + 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) + } + } + } + /** * 下一页 */ 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 8d8153f..39a3895 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 @@ -155,31 +155,9 @@ fun AboutScreen( ) } } + Spacer(modifier = Modifier.height(16.dp)) - // 技术栈 - // Card( - // modifier = Modifier.fillMaxWidth() - // ) { - // Column( - // modifier = Modifier.padding(16.dp) - // ) { - // Text( - // text = stringResource(Res.string.tech_stack), - // style = MaterialTheme.typography.titleMedium, - // fontWeight = FontWeight.Medium - // ) - // - // Spacer(modifier = Modifier.height(12.dp)) - // - // TechStackItem("Kotlin Multiplatform", Res.string.tech_stack_kmp) - // TechStackItem("Jetpack Compose", Res.string.tech_stack_compose) - // TechStackItem("Room Database", Res.string.tech_stack_room) - // TechStackItem("Koin", Res.string.tech_stack_koin) - // TechStackItem("Ktor", Res.string.tech_stack_ktor) - // TechStackItem("MVI Architecture", Res.string.tech_stack_mvi) - // } - // } - // Spacer(modifier = Modifier.height(16.dp)) + // 开发者信息 Card( modifier = Modifier.fillMaxWidth(), @@ -202,7 +180,9 @@ fun AboutScreen( ) } } + Spacer(modifier = Modifier.height(16.dp)) + // 联系方式 Card( modifier = Modifier.fillMaxWidth(), @@ -277,26 +257,6 @@ private fun AboutInfoRow( } } -@Composable -private fun TechStackItem( - name: String, - descriptionRes: StringResource, -) { - Column { - Text( - text = name, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium - ) - Text( - text = stringResource(descriptionRes), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(8.dp)) - } -} - @Composable private fun ContactItem( icon: ImageVector, diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackViewModel.kt index 47ec187..3160e07 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackViewModel.kt @@ -1,5 +1,6 @@ package com.taskttl.presentation.features.settings.feedback +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel import com.taskttl.data.source.remote.api.TaskTTLApi @@ -16,6 +17,7 @@ import taskttl.composeapp.generated.resources.feedback_success * @date 2025/10/12 * @constructor 创建[FeedbackViewModel] */ +@Stable class FeedbackViewModel() : BaseViewModel(FeedbackState()) { 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 dfa5912..d5c6fb4 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 @@ -23,6 +23,8 @@ import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Storage +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -48,10 +50,10 @@ import com.taskttl.core.permission.NotificationPermissionManager import com.taskttl.navigation.Routes import com.taskttl.presentation.common.components.AppHeader import com.taskttl.presentation.common.components.ThemeModeDialog +import com.taskttl.presentation.features.settings.main.common.UserInfoCard import kotlinx.coroutines.delay import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.section_data_management @@ -80,7 +82,6 @@ import taskttl.composeapp.generated.resources.title_app_settings * @param [navController] 导航控制器 */ @Composable -@Preview fun SettingsScreen( navController: NavHostController, viewModel: SettingsViewModel = koinViewModel(), @@ -120,85 +121,23 @@ fun SettingsScreen( .verticalScroll(rememberScrollState()) .padding(16.dp) ) { - // 用户信息卡片 - Column( - modifier = Modifier - .fillMaxWidth() - .background( - brush = Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.secondary - ) - ), - shape = RoundedCornerShape(16.dp) - ) - .padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally + // 用户信息卡片(登录前/后通用组件) + UserInfoCard( + isLoggedIn = true, + userName = "DevTTL", + userSubtitle = "111111", + onClick = { navController.navigate(Routes.Main.Settings.Login) } + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Card( + modifier = Modifier.fillMaxWidth().height(80.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ) ) { - Row( - modifier = Modifier.clickable { navController.navigate(Routes.Main.Settings.Login) }, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .size(60.dp) - .background(Color.White.copy(alpha = 0.2f), shape = CircleShape), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.Person, - contentDescription = "用户头像", - tint = Color.White, - modifier = Modifier.size(40.dp) - ) - // AsyncImage( - // model = Res.getUri("drawable/ic_launcher.png"), - // contentDescription = null, - // modifier = Modifier - // .size(120.dp), - // contentScale = ContentScale.Fit - // ) - } - Spacer(modifier = Modifier.width(12.dp)) - Column( - modifier = Modifier.weight(1f).height(60.dp), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.SpaceBetween - ) { - Text( - "登录或注册", - color = Color.White, - fontWeight = FontWeight.Medium, - fontSize = 18.sp - ) - Text( - "加入TaskTTL", - color = Color.White.copy(alpha = 0.9f), - fontSize = 14.sp - ) - } - - // Column( - // modifier = Modifier.weight(1f).height(60.dp), - // horizontalAlignment = Alignment.End, - // verticalArrangement = Arrangement.SpaceBetween - // ) { - // Text( - // "昵称", - // color = Color.White, - // fontWeight = FontWeight.Medium, - // fontSize = 18.sp - // ) - // Text( - // "已使用 30 天 · 完成 156 个任务", - // color = Color.White.copy(alpha = 0.9f), - // fontSize = 14.sp - // ) - // } - } } Spacer(modifier = Modifier.height(24.dp)) @@ -252,7 +191,7 @@ fun SettingsScreen( ) Spacer(modifier = Modifier.height(16.dp)) - // // 社交分享 + // 社交分享 // SectionTitle(Icons.Default.Share, Res.string.section_social_share) // SettingItem( // titleRes = Res.string.setting_share_achievement, @@ -378,7 +317,11 @@ fun SettingItem( ) descriptionRes.let { Spacer(modifier = Modifier.height(2.dp)) - Text(stringResource(it), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + stringResource(it), + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } } 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 8b6c631..0353dd2 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 @@ -1,5 +1,6 @@ package com.taskttl.presentation.features.settings.main +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel import com.taskttl.core.manager.ThemeManager @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch * @date 2025/10/12 * @constructor 创建[SettingsViewModel] */ +@Stable class SettingsViewModel(private val themeManager: ThemeManager) : BaseViewModel(SettingsState()) { 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 new file mode 100644 index 0000000..107ac02 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt @@ -0,0 +1,126 @@ +package com.taskttl.presentation.features.settings.main.common + +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.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.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 +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.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.presentation.common.components.NetworkImage +import taskttl.composeapp.generated.resources.Res + +/** + * 用户信息卡片 + * 支持: + * - 未登录:展示登录引导 + * - 已登录:展示头像首字母、昵称、副标题 + */ +@Composable +fun UserInfoCard( + isLoggedIn: Boolean, + userName: String?, + userSubtitle: String?, + onClick: () -> Unit, +) { + + Row( + modifier = Modifier.fillMaxWidth() + .padding(16.dp) + .clickable { onClick() }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // 左侧文本 + Column( + modifier = Modifier.weight(1f).height(60.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceBetween + ) { + if (isLoggedIn && !userName.isNullOrBlank()) { + Text( + text = userName, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) + Text( + text = userSubtitle ?: "欢迎回来,继续保持高效!", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp + ) + } else { + Text( + text = "登录或注册", + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) + Text( + text = "加入 TaskTTL,跨设备同步你的任务与倒计时", + color = MaterialTheme.colorScheme.onSurface, + fontSize = 14.sp + ) + } + } + + if (!isLoggedIn) { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = Color.White.copy(alpha = 0.9f) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + // 头像/占位 + Box( + modifier = Modifier + .size(60.dp).clip(CircleShape) + .background(Color.White.copy(alpha = 0.2f), shape = CircleShape), + contentAlignment = Alignment.Center + ) { + if (isLoggedIn && !userName.isNullOrBlank()) { + NetworkImage( + url = Res.getUri("drawable/ic_launcher.png"), + contentDescription = null, + modifier = Modifier.size(60.dp), + contentScale = ContentScale.Fit + ) + } else { + Icon( + imageVector = Icons.Default.Person, + contentDescription = "用户头像", + tint = Color.White, + modifier = Modifier.size(40.dp) + ) + } + } + + } +} \ No newline at end of file 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 9205fc7..495cd2a 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 @@ -34,7 +34,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.taskttl.navigation.Routes import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.app_name @@ -45,7 +44,6 @@ import taskttl.composeapp.generated.resources.app_name_remark * @param [navigatorToRoute] 导航到路线 * @param [viewModel] 视图模型 */ -@Preview @Composable fun SplashScreen( navigatorToRoute: (Routes) -> Unit, 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 b1b7cb8..0defe63 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 @@ -16,7 +16,12 @@ data class SplashState( ) : BaseState() sealed class SplashIntent { - object LoadApp : SplashIntent() + /** + * 装载状态 + * @author DevTTL + * @date 2025/11/07 + */ + object LoadingStatus : SplashIntent() } 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 e567bc9..9f2bffc 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 @@ -1,51 +1,58 @@ package com.taskttl.presentation.features.splash +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel import com.taskttl.core.domain.constant.PointEvent import com.taskttl.core.utils.DeviceUtils +import com.taskttl.core.utils.LogUtils import com.taskttl.core.utils.StorageUtils import com.taskttl.data.source.remote.api.TaskTTLApi import com.taskttl.domain.repository.OnboardingRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * 启动页视图模型 * @author admin * @date 2025/08/11 * @constructor 创建[SplashViewModel] - * @param [settings] 挡 */ +@Stable class SplashViewModel( private val onboardingRepository: OnboardingRepository, ) : BaseViewModel(SplashState()) { init { - processIntent(SplashIntent.LoadApp) + processIntent(SplashIntent.LoadingStatus) } + /** + * 处理意图 + * @param [intent] 意图 + */ override fun handleIntent(intent: SplashIntent) { when (intent) { - is SplashIntent.LoadApp -> loadApp() + is SplashIntent.LoadingStatus -> loadingStatus() } } - private fun loadApp() { + /** + * 装载状态 + */ + private fun loadingStatus() { viewModelScope.launch { - 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) - } + delay(200) val hasLaunched = onboardingRepository.isLaunchedBefore() - if (hasLaunched) { - sendEvent(SplashEffect.NavigateToOnboarding) - } else { - sendEvent(SplashEffect.NavigateToMain) + LogUtils.e("DevTTL",hasLaunched.toString()) + withContext(Dispatchers.Main) { + if (hasLaunched) { + sendEvent(SplashEffect.NavigateToOnboarding) + } else { + sendEvent(SplashEffect.NavigateToMain) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/CategoryStatisticsCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/CategoryStatisticsCard.kt deleted file mode 100644 index c58baca..0000000 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/CategoryStatisticsCard.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.taskttl.ui.components - -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.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.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.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.taskttl.domain.model.CategoryStatistics -import org.jetbrains.compose.resources.stringResource -import taskttl.composeapp.generated.resources.Res -import taskttl.composeapp.generated.resources.active -import taskttl.composeapp.generated.resources.completed -import taskttl.composeapp.generated.resources.completion_rate -import taskttl.composeapp.generated.resources.total_countdowns -import taskttl.composeapp.generated.resources.total_tasks - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CategoryStatisticsCard( - statistics: CategoryStatistics, - categoryColor: Long, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(CircleShape) - .background(Color(categoryColor)) - ) - - Spacer(modifier = Modifier.width(12.dp)) - - Text( - text = statistics.categoryName, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 任务统计 - if (statistics.totalTasks > 0) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column { - Text( - text = stringResource(Res.string.total_tasks), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${statistics.totalTasks}", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - } - - Column { - Text( - text = stringResource(Res.string.completed), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${statistics.completedTasks}", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - } - - Column { - Text( - text = stringResource(Res.string.completion_rate), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${(statistics.completionRate * 100).toInt()}%", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = if (statistics.completionRate >= 0.8f) - MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurface - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - // 进度条 - LinearProgressIndicator( - progress = { statistics.completionRate }, - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .clip(RoundedCornerShape(4.dp)), - color = Color(categoryColor), - trackColor = ProgressIndicatorDefaults.linearTrackColor, - strokeCap = ProgressIndicatorDefaults.LinearStrokeCap, - ) - } - - // 倒数日统计 - if (statistics.totalCountdowns > 0) { - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column { - Text( - text = stringResource(Res.string.total_countdowns), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${statistics.totalCountdowns}", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - } - - Column { - Text( - text = stringResource(Res.string.active), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${statistics.activeCountdowns}", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - } - } - } - } - } -} \ No newline at end of file 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 2e58d31..171e48a 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 @@ -52,7 +52,6 @@ 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.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.category_countdown @@ -66,7 +65,6 @@ import taskttl.composeapp.generated.resources.title_statistics import taskttl.composeapp.generated.resources.total_tasks @Composable -@Preview fun StatisticsScreen( navController: NavHostController, taskViewModel: TaskViewModel = koinViewModel(), diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/detail/TaskEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/detail/TaskEditorScreen.kt index b651624..0e28c8e 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/detail/TaskEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/detail/TaskEditorScreen.kt @@ -1,4 +1,5 @@ package com.taskttl.presentation.features.task.detail +// 任务编辑 import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -56,13 +57,13 @@ import com.taskttl.presentation.features.task.list.TaskIntent import com.taskttl.presentation.features.task.list.TaskViewModel import com.taskttl.ui.components.CategoryCard import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.number import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.hint_tags @@ -79,13 +80,18 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +/** + * 任务编辑器屏幕 + * @param [taskId] 任务id + * @param [onNavigateBack] 返回导航 + * @param [viewModel] 视图模型 + */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class, ExperimentalUuidApi::class) @Composable -@Preview fun TaskEditorScreen( taskId: String? = null, onNavigateBack: () -> Unit, - viewModel: TaskViewModel = koinViewModel() + viewModel: TaskViewModel = koinViewModel(), ) { LaunchedEffect(Unit) { @@ -306,15 +312,18 @@ fun TaskEditorScreen( fun DueDateSelector( dueDate: LocalDateTime?, onDateSelected: (LocalDateTime?) -> Unit, - onClick: () -> Unit + onClick: () -> Unit, ) { val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) val today = now.date val tomorrow = today.plus(1, DateTimeUnit.DAY) val dateFormatter = { date: LocalDateTime -> - "${date.year}-${date.month.number.toString().padStart(2, '0')}-${date.day.toString().padStart(2, '0')} " + - "${date.hour.toString().padStart(2, '0')}:${date.minute.toString().padStart(2, '0')}" + "${date.year}-${date.month.number.toString().padStart(2, '0')}-${ + date.day.toString().padStart(2, '0') + } " + "${date.hour.toString().padStart(2, '0')}:${ + date.minute.toString().padStart(2, '0') + }" } Column(modifier = Modifier.fillMaxWidth()) { @@ -334,7 +343,7 @@ fun DueDateSelector( Button( onClick = { onDateSelected( - LocalDateTime(date.year, date.month, date.dayOfMonth, now.hour, now.minute) + LocalDateTime(date.year, date.month, date.day, now.hour, now.minute) ) }, modifier = Modifier.weight(1f), @@ -349,10 +358,8 @@ fun DueDateSelector( // 其他时间按钮占剩余空间 Button( onClick = onClick, - modifier = Modifier.fillMaxWidth(), // 占满剩余空间 - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.surfaceVariant) ) { Text("其他时间") } @@ -362,7 +369,11 @@ fun DueDateSelector( Box( modifier = Modifier .fillMaxWidth() - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + MaterialTheme.shapes.small + ) .padding(horizontal = 12.dp, vertical = 8.dp) ) { Row( diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/editor/TaskDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/editor/TaskDetailScreen.kt index 5217b26..27ffcc2 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/editor/TaskDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/editor/TaskDetailScreen.kt @@ -1,4 +1,5 @@ package com.taskttl.presentation.features.task.editor +// 任务详细信息屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -54,6 +55,13 @@ import taskttl.composeapp.generated.resources.label_none import taskttl.composeapp.generated.resources.text_task_not_found import taskttl.composeapp.generated.resources.title_task_info +/** + * 任务详细信息屏幕 + * @param [taskId] 任务id + * @param [onNavigateBack] 返回导航 + * @param [onNavigateToEdit] 导航到编辑 + * @param [viewModel] 视图模型 + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TaskDetailScreen( 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 08c01e5..475f0b9 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 @@ -1,5 +1,5 @@ package com.taskttl.presentation.features.task.list - +// 任务屏幕 import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -43,7 +43,6 @@ import com.taskttl.presentation.common.components.LoadingOverlay import com.taskttl.presentation.common.components.SearchBar import com.taskttl.presentation.features.task.list.components.TaskCardItem import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.label_show_completed @@ -53,8 +52,12 @@ import taskttl.composeapp.generated.resources.text_no_tasks import taskttl.composeapp.generated.resources.title_add_task import taskttl.composeapp.generated.resources.title_task +/** + * 任务屏幕 + * @param [navController] 导航控制器 + * @param [viewModel] 视图模型 + */ @Composable -@Preview fun TaskScreen( navController: NavHostController, viewModel: TaskViewModel = koinViewModel(), 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 3739a23..9b65725 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 @@ -1,8 +1,9 @@ package com.taskttl.presentation.features.task.list - +// 任务状态 import com.taskttl.core.base.BaseState import com.taskttl.domain.model.Category import com.taskttl.domain.model.Task +import kotlinx.serialization.Serializable /** * 任务状态 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskViewModel.kt index b18d784..55205ef 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskViewModel.kt @@ -1,5 +1,6 @@ package com.taskttl.presentation.features.task.list +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.taskttl.core.base.BaseViewModel import com.taskttl.core.notification.AppNotificationManager @@ -34,6 +35,7 @@ import kotlin.time.ExperimentalTime * @constructor 创建[TaskViewModel] * @param [taskRepository] 任务仓库 */ +@Stable class TaskViewModel( private val taskRepository: TaskRepository, private val categoryRepository: CategoryRepository, diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/components/TaskCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/components/TaskCard.kt index 51aa561..ece9c3c 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/components/TaskCard.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/components/TaskCard.kt @@ -1,4 +1,5 @@ package com.taskttl.presentation.features.task.list.components +// 任务卡片 import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/composeApp/src/google-services.json b/composeApp/src/google-services.json index 1671afe..6c54e1b 100644 --- a/composeApp/src/google-services.json +++ b/composeApp/src/google-services.json @@ -23,6 +23,25 @@ "other_platform_oauth_client": [] } } + }, + { + "client_info": { + "mobilesdk_app_id": "1:324197572456:android:89abdcdd4e5892872440dd", + "android_client_info": { + "package_name": "com.taskttl.debug" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBPQBhVV-aMHpvyT-S3VgjXN__Dj0Z-Jes" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } } ], "configuration_version": "1" diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/app/di/AppModule.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/app/di/AppModule.jvm.kt new file mode 100644 index 0000000..c8e76fe --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/app/di/AppModule.jvm.kt @@ -0,0 +1,9 @@ +package com.taskttl.app.di + +import com.taskttl.core.database.TaskTTLDatabase +import com.taskttl.core.database.getDatabaseBuilder +import org.koin.dsl.module + +actual val serviceModule = module { + single { getDatabaseBuilder() } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/common/DevTTLWebView.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/common/DevTTLWebView.jvm.kt new file mode 100644 index 0000000..99ffbb5 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/core/common/DevTTLWebView.jvm.kt @@ -0,0 +1,8 @@ +package com.taskttl.core.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +actual fun DevTTLWebView(modifier: Modifier, url: String, enableJavaScript: Boolean) { +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/database/Database.desktop.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/database/Database.desktop.kt new file mode 100644 index 0000000..449f9bd --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/core/database/Database.desktop.kt @@ -0,0 +1,14 @@ +package com.taskttl.core.database + +import androidx.room.Room +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import kotlinx.coroutines.Dispatchers +import java.io.File + +actual fun getDatabaseBuilder(): TaskTTLDatabase { + val dbFile = File(System.getProperty("java.io.tmpdir"), "taskttl_dababase.db") + return Room.databaseBuilder(name = dbFile.absolutePath) + .setDriver(BundledSQLiteDriver()) + .setQueryCoroutineContext(Dispatchers.IO) + .build() +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/ExactAlarmPermissionManager.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/ExactAlarmPermissionManager.jvm.kt index 94d8409..e00ecce 100644 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/ExactAlarmPermissionManager.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/ExactAlarmPermissionManager.jvm.kt @@ -2,10 +2,12 @@ package com.taskttl.core.permission actual object ExactAlarmPermissionManager { actual fun requestPermission(): Boolean { + return false TODO("Not yet implemented") } actual fun verifyPermission(): Boolean { + return false TODO("Not yet implemented") } diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/NotificationPermissionManager.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/NotificationPermissionManager.jvm.kt index 11fb093..4ffad35 100644 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/NotificationPermissionManager.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/core/permission/NotificationPermissionManager.jvm.kt @@ -2,10 +2,12 @@ package com.taskttl.core.permission actual object NotificationPermissionManager { actual fun requestPermission(): Boolean { + return false TODO("Not yet implemented") } actual fun verifyPermission(): Boolean { + return false TODO("Not yet implemented") } diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/ui/DevTTLWebView.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/ui/DevTTLWebView.jvm.kt deleted file mode 100644 index 5e8db9f..0000000 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/core/ui/DevTTLWebView.jvm.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.taskttl.core.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -actual fun DevTTLWebView(modifier: Modifier, url: String) { -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/DeviceUtils.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/DeviceUtils.jvm.kt index fc57997..69d31e9 100644 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/DeviceUtils.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/DeviceUtils.jvm.kt @@ -4,10 +4,19 @@ import com.taskttl.core.domain.BaseReq actual object DeviceUtils { actual suspend fun getUniqueId(): String { - TODO("Not yet implemented") + return "123" } actual suspend fun getDeviceInfo(): BaseReq { - TODO("Not yet implemented") + // TODO + return BaseReq( + appName = "TaskTTL", + versionCode = 999, + appId = 100, + uniqueId = getUniqueId(), + deviceInfo = "0", + deviceVersion = "11", + language = Localization.getDeviceLanguage() + ) } } \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/Localization.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/Localization.jvm.kt index e8c67b9..fe8e236 100644 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/Localization.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/Localization.jvm.kt @@ -1,10 +1,13 @@ package com.taskttl.core.utils +import java.util.Locale + actual object Localization { + actual fun applyLanguage(iso: String) { } actual fun getDeviceLanguage(): String { - TODO("Not yet implemented") + return Locale.getDefault().language } } \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/StorageUtils.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/StorageUtils.jvm.kt deleted file mode 100644 index f976e7c..0000000 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/core/utils/StorageUtils.jvm.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.taskttl.core.utils - -actual object StorageUtils { - actual fun saveString(key: String, value: String) { - } - - actual fun getString(key: String, defaultValue: String): String { - TODO("Not yet implemented") - } - - actual fun saveInt(key: String, value: Int) { - } - - actual fun getInt(key: String, defaultValue: Int): Int { - TODO("Not yet implemented") - } - - actual fun saveLong(key: String, value: Long) { - } - - actual fun getLong(key: String, defaultValue: Long): Long { - TODO("Not yet implemented") - } - - actual fun saveBoolean(key: String, value: Boolean) { - } - - actual fun getBoolean(key: String, defaultValue: Boolean): Boolean { - TODO("Not yet implemented") - } - - actual inline fun saveObject(key: String, value: T) { - } - - actual inline fun getObject(key: String): T? { - TODO("Not yet implemented") - } - - actual fun contains(key: String): Boolean { - TODO("Not yet implemented") - } - - actual fun remove(key: String) { - } - - actual fun clear() { - } -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/data/di/AppModule.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/data/di/AppModule.jvm.kt deleted file mode 100644 index fc176e8..0000000 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/data/di/AppModule.jvm.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.taskttl.data.di - -import com.taskttl.data.local.database.TaskTTLDatabase -import com.taskttl.data.local.database.getDatabaseBuilder -import org.koin.dsl.module - -actual val serviceModule = module { - single { getDatabaseBuilder() } -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/data/local/database/Database.desktop.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/data/local/database/Database.desktop.kt deleted file mode 100644 index 4926e43..0000000 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/data/local/database/Database.desktop.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.taskttl.data.local.database - -import androidx.room.Room -import java.io.File - -actual fun getDatabaseBuilder(): TaskTTLDatabase { - val dbFile = File(System.getProperty("java.io.tmpdir"), "taskttl_dababase.db") - return Room.databaseBuilder( - name = dbFile.absolutePath, - ).build() -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/data/repository/AuthRepository.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/data/repository/AuthRepository.jvm.kt deleted file mode 100644 index 6247182..0000000 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/data/repository/AuthRepository.jvm.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.taskttl.data.repository - -import com.taskttl.data.source.remote.dto.response.AuthResult - -actual class AuthRepository actual constructor() { - actual suspend fun loginWithGoogle(): AuthResult { - TODO("Not yet implemented") - } - - actual suspend fun loginWithFacebook(): AuthResult { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/domain/repository/AuthRepository.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/domain/repository/AuthRepository.jvm.kt new file mode 100644 index 0000000..d2abfa6 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/domain/repository/AuthRepository.jvm.kt @@ -0,0 +1,14 @@ +package com.taskttl.domain.repository + +import com.taskttl.data.source.remote.dto.response.AuthResult + +class AuthRepositoryImpl : AuthRepository { + + override suspend fun loginWithGoogle(): AuthResult { + TODO("Not yet implemented") + } + + override suspend fun loginWithFacebook(): AuthResult { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/main.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/main.kt index 4953bfa..440cdd3 100644 --- a/composeApp/src/jvmMain/kotlin/com/taskttl/main.kt +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/main.kt @@ -2,8 +2,14 @@ package com.taskttl import androidx.compose.ui.window.Window import androidx.compose.ui.window.application +import com.taskttl.app.App +import com.taskttl.app.di.initKoin fun main() = application { + + initKoin { + } + Window( onCloseRequest = ::exitApplication, alwaysOnTop = true, diff --git a/composeApp/src/jvmMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.jvm.kt b/composeApp/src/jvmMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.jvm.kt new file mode 100644 index 0000000..5921c52 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.jvm.kt @@ -0,0 +1,12 @@ +package com.taskttl.presentation.common.foundation + +import okio.Path +import okio.Path.Companion.toOkioPath +import java.io.File + +actual fun provideDiskCachePath(): Path? { + val userHome = System.getProperty("user.home") + val cacheDir = File(userHome, ".cache/myapp") + if (!cacheDir.exists()) cacheDir.mkdirs() + return cacheDir.toOkioPath() +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 587bc43..35206f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,22 @@ [versions] -agp = "8.13.0" +agp = "8.13.1" androidx-activity = "1.11.0" androidx-appcompat = "1.7.1" androidx-constraintlayout = "2.2.1" androidx-core = "1.17.0" androidx-espresso = "3.7.0" -androidx-lifecycle = "2.9.5" +androidx-lifecycle = "2.9.6" androidx-testExt = "1.3.0" composeHotReload = "1.0.0-rc02" -composeMultiplatform = "1.9.1" +composeMultiplatform = "1.10.0-beta01" junit = "4.13.2" kotlin = "2.2.21" kotlinx-coroutines = "1.10.2" +splashscreen = "1.2.0" navigationCompose = "2.9.1" koin = "4.1.1" -ktor = "3.3.1" +ktor = "3.3.2" coil3 = "3.3.0" kotlinx-datetime = "0.7.1" icons = "1.7.3" @@ -23,12 +24,12 @@ icons = "1.7.3" google = "4.4.4" credentials = "1.6.0-beta03" googleid = "1.1.1" -firebase = "34.4.0" +firebase = "34.6.0" facebook = "18.1.3" playServicesAds = "18.2.0" kotlinx-serialization = "1.9.0" -mmkv = "2.2.4" +settings = "1.3.0" sqlite = "2.6.1" room = "2.8.3" @@ -63,9 +64,13 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" } +androidx-lifecycle-savedstate = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +# 启动兼容 +androidx-splashscreen = {module = "androidx.core:core-splashscreen" , version.ref = "splashscreen"} + # 导航 navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } @@ -80,7 +85,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } -#implementation("io.ktor:ktor-client-cio:2.3.12") # 桌面 +ktor-client-java = {module = "io.ktor:ktor-client-java", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } @@ -107,8 +112,9 @@ android-facebook-android-sdk = { module = "com.facebook.android:facebook-android # JSON kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } -# 安卓MMKV -android-mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } +# Settings +multiplatform-settings = {module = "com.russhwolf:multiplatform-settings",version.ref="settings"} +multiplatform-settings-no-arg = {module = "com.russhwolf:multiplatform-settings-no-arg", version.ref="settings"} # Room数据库 androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } @@ -123,7 +129,6 @@ android-play-services-ads-identifier = { module = "com.google.android.gms:play-s androidx-work = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } # 三方登录 -#androidx-login-google = { module = "com.google.android.gms:play-services-auth", version.ref = "google-login" } androidx-login-credentials = { module = "androidx.credentials:credentials", version.ref = "credentials" } androidx-login-credentials-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentials" } androidx-login-googleid = {module = "com.google.android.libraries.identity.googleid:googleid",version.ref="googleid"} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e11132..bad7c24 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME