更新
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:theme="@style/Theme.Splash"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
<!-- AlarmReceiver -->
|
||||
<receiver
|
||||
android:name=".core.alarm.AlarmReceiver"
|
||||
android:name=".core.receiver.AlarmReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
>
|
||||
@@ -77,7 +78,7 @@
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".core.alarm.BootReceiver"
|
||||
android:name=".core.receiver.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<TaskTTLDatabase> { getDatabaseBuilder() }
|
||||
|
||||
singleOf(::AuthRepositoryImpl).bind(AuthRepository::class)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.alarm
|
||||
package com.taskttl.core.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.alarm
|
||||
package com.taskttl.core.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -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!!)
|
||||
|
||||
// 2) ANDROID_ID
|
||||
val ssaid = getAndroidIdSafe()
|
||||
if (!ssaid.isNullOrBlank()) return persistAndReturn(ssaid)
|
||||
|
||||
// 3) UUID
|
||||
val uuid = UUID.randomUUID().toString().replace("-", "")
|
||||
return persistAndReturn(uuid)
|
||||
}
|
||||
|
||||
val uniqueId = if (isValidAdId(adId)) {
|
||||
adId.toString()
|
||||
} else {
|
||||
// 如果广告ID无效,生成应用实例ID
|
||||
generateAppInstanceId()
|
||||
}
|
||||
|
||||
// 保存生成的ID
|
||||
StorageUtils.saveString(PREF_UNIQUE_ID, uniqueId)
|
||||
/**
|
||||
* 持久化并返回
|
||||
* @param [uniqueId] 唯一ID
|
||||
* @return [String]
|
||||
*/
|
||||
private fun persistAndReturn(uniqueId: String): String {
|
||||
StorageUtils.saveString(Constant.PREF_UNIQUE_ID, uniqueId)
|
||||
return uniqueId
|
||||
}
|
||||
|
||||
@@ -73,21 +73,22 @@ actual object DeviceUtils {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取谷歌广告id
|
||||
* @return [String?]
|
||||
*/
|
||||
@SuppressLint("AdvertisingIdPolicy")
|
||||
private suspend fun getGoogleAdId(): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
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 (e: TimeoutCancellationException) {
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证是否是有效广告id
|
||||
@@ -95,50 +96,27 @@ actual object DeviceUtils {
|
||||
* @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<String>()
|
||||
// 设备硬件信息
|
||||
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 {
|
||||
|
||||
@@ -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 <reified T : Any> saveObject(key: String, value: T) {
|
||||
val data = json.encodeToString(value)
|
||||
mmkv.encode(key, data)
|
||||
}
|
||||
|
||||
actual inline fun <reified T : Any> getObject(key: String): T? {
|
||||
val data = mmkv.decodeString(key) ?: return null
|
||||
return try {
|
||||
json.decodeFromString<T>(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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 登录失败"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
18
composeApp/src/androidMain/res/values-v31/styles.xml
Normal file
18
composeApp/src/androidMain/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.SplashScreenBase" parent="Theme.SplashScreen">
|
||||
<!-- ##必填##!!!在Splash结束后要套用的主题 请将原本Activtiy设定的Theme放在这边-->
|
||||
<item name="postSplashScreenTheme">@android:style/Theme.Material.Light.NoActionBar</item>
|
||||
<!-- 选填 Splash画面的背景颜色 -->
|
||||
<item name="windowSplashScreenBackground">#FF5B6FF8</item>
|
||||
<!-- 选填 显示在Splash画面上的Icon 可以用 一般的Icon、AnimationDrawable、AnimatedVectorDrawable -->
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_round</item>
|
||||
<!-- 选填 如果你的Icon是有动画的可以透过这个来改完整的动画时间 -->
|
||||
<item name="android:windowSplashScreenAnimationDuration">1000</item>
|
||||
<!-- 选填 Icon圆圈圈后面的背景色 如果你的Icon与原背景太过相近可以透过这个调背景色 -->
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FF8B5FBF</item>
|
||||
<!-- 选填 显示在Splash画面上底部的Icon -->
|
||||
<item name="android:windowSplashScreenBrandingImage">@mipmap/ic_launcher_round</item>
|
||||
</style>
|
||||
</resources>
|
||||
12
composeApp/src/androidMain/res/values/styles.xml
Normal file
12
composeApp/src/androidMain/res/values/styles.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Splash" parent="Theme.SplashScreen">
|
||||
<!-- ##必填##!!!在Splash结束后要套用的主题 请将原本Activtiy设定的Theme放在这边-->
|
||||
<item name="postSplashScreenTheme">@android:style/Theme.Material.Light.NoActionBar</item>
|
||||
<!-- 选填 Splash画面的背景颜色 -->
|
||||
<item name="windowSplashScreenBackground">#FF5B6FF8</item>
|
||||
<item name="windowSplashScreenIconBackgroundColor">#FF5B6FF8</item>
|
||||
<!-- 选填 显示在Splash画面上的Icon 可以用 一般的Icon、AnimationDrawable、AnimatedVectorDrawable -->
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_round</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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, AppIntent, AppEffect>(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) }
|
||||
|
||||
@@ -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<TaskRepository> { TaskRepositoryImpl(get(), get()) }
|
||||
single<CountdownRepository> { CountdownRepositoryImpl(get(), get()) }
|
||||
single<CategoryRepository> { CategoryRepositoryImpl(get(), get(), get(), get()) }
|
||||
|
||||
singleOf(::AuthRepository)
|
||||
singleOf(::TaskRepositoryImpl).bind(TaskRepository::class)
|
||||
singleOf(::CountdownRepositoryImpl).bind(CountdownRepository::class)
|
||||
singleOf(::CategoryRepositoryImpl).bind(CategoryRepository::class)
|
||||
}
|
||||
@@ -28,7 +28,9 @@ import org.koin.dsl.module
|
||||
*/
|
||||
fun initKoin(config: (KoinApplication.() -> Unit)? = null) {
|
||||
startKoin {
|
||||
// 先执行用户自定义配置
|
||||
config?.invoke(this)
|
||||
// 再加载模块
|
||||
modules(repositoryModule, viewModelModule, serviceModule, dataModule)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@ import androidx.compose.ui.Modifier
|
||||
* @param [url] 网址
|
||||
*/
|
||||
@Composable
|
||||
expect fun DevTTLWebView(modifier: Modifier, url: String)
|
||||
expect fun DevTTLWebView(modifier: Modifier, url: String,enableJavaScript: Boolean = true)
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 <reified T : Any> saveObject(key: String, value: T)
|
||||
inline fun <reified T : Any> saveObject(key: String, value: T) {
|
||||
settings[key] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象
|
||||
* @param key 键
|
||||
* @return 存储的对象或null
|
||||
*/
|
||||
inline fun <reified T : Any> getObject(key: String): T?
|
||||
inline fun <reified T : Any> 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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.taskttl.data.constant
|
||||
|
||||
/**
|
||||
* 提供者枚举
|
||||
* @author DevTTL
|
||||
* @date 2025/11/13
|
||||
* @constructor 创建[ProviderEnum]
|
||||
*/
|
||||
enum class ProviderEnum {
|
||||
GOOGLE,
|
||||
FACEBOOK,
|
||||
APPLE,
|
||||
GITHUB
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.taskttl.navigation
|
||||
|
||||
// 应用导航
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
@@ -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<Routes.Main.Task> {
|
||||
TaskScreen(navController = mainNavController)
|
||||
}
|
||||
@@ -114,7 +113,6 @@ fun MainNav() {
|
||||
)
|
||||
}
|
||||
|
||||
// 倒数日
|
||||
composable<Routes.Main.Countdown> {
|
||||
CountdownScreen(navController = mainNavController)
|
||||
}
|
||||
@@ -147,11 +145,9 @@ fun MainNav() {
|
||||
StatisticsScreen(navController = mainNavController)
|
||||
}
|
||||
|
||||
// 设置界面
|
||||
composable<Routes.Main.Settings> {
|
||||
SettingsScreen(navController = mainNavController)
|
||||
}
|
||||
// 分类管理
|
||||
composable<Routes.Main.Settings.CategoryManagement> { backStackEntry ->
|
||||
CategoryScreen(
|
||||
navController = mainNavController,
|
||||
@@ -159,14 +155,12 @@ fun MainNav() {
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 添加分类
|
||||
composable<Routes.Main.Settings.AddCategory> {
|
||||
CategoryEditScreen(
|
||||
categoryId = null,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 编辑分类
|
||||
composable<Routes.Main.Settings.EditCategory> { backStackEntry ->
|
||||
val editCategory: Routes.Main.Settings.EditCategory = backStackEntry.toRoute()
|
||||
CategoryEditScreen(
|
||||
@@ -174,28 +168,23 @@ fun MainNav() {
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 数据管理
|
||||
composable<Routes.Main.Settings.DataManagement> {
|
||||
DataManagementScreen(onNavigateBack = { mainNavController.popBackStack() })
|
||||
}
|
||||
// 反馈页面
|
||||
composable<Routes.Main.Settings.Feedback> {
|
||||
FeedbackScreen(onNavigateBack = { mainNavController.popBackStack() })
|
||||
}
|
||||
// 隐私
|
||||
composable<Routes.Main.Settings.Privacy> {
|
||||
PrivacyScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 关于页面
|
||||
composable<Routes.Main.Settings.About> {
|
||||
AboutScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
// 登录页面
|
||||
composable<Routes.Main.Settings.Login> {
|
||||
LoginScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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?
|
||||
@@ -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 {}
|
||||
sealed class AuthEffect {
|
||||
/**
|
||||
* 显示消息
|
||||
* @author admin
|
||||
* @date 2025/09/27
|
||||
* @constructor 创建[ShowMessage]
|
||||
* @param [message] 消息
|
||||
*/
|
||||
data class ShowMessage(val message: String) : AuthEffect()
|
||||
}
|
||||
@@ -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, AuthIntent, AuthEffect>(AuthState()) {
|
||||
|
||||
|
||||
public override fun handleIntent(intent: AuthIntent) {
|
||||
when (intent) {
|
||||
AuthIntent.LoginWithGoogle -> {
|
||||
// TODO: 调用 Google 登录逻辑
|
||||
println("Google 登录触发")
|
||||
viewModelScope.launch {
|
||||
authRepository.loginWithGoogle()
|
||||
is AuthIntent.LoginWithGoogle -> onLoginWithGoogle()
|
||||
|
||||
is AuthIntent.LoginWithFacebook -> onLoginWithFacebook()
|
||||
|
||||
is AuthIntent.ClearError -> clearError()
|
||||
is AuthIntent.Logout -> {}
|
||||
}
|
||||
}
|
||||
|
||||
AuthIntent.LoginWithFacebook -> {
|
||||
// TODO: 调用 Facebook 登录逻辑
|
||||
println("Facebook 登录触发")
|
||||
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 {
|
||||
authRepository.loginWithFacebook()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,74 +103,19 @@ 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
|
||||
) {
|
||||
// --- 登录卡片 ---
|
||||
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
|
||||
// 应用 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
|
||||
},
|
||||
modifier = Modifier.size(120.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
@@ -165,27 +137,27 @@ fun LoginScreen(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
AnimatedLoginButton(
|
||||
// Google 登录按钮
|
||||
LoginButton(
|
||||
icon = Icons.Default.Language,
|
||||
text = "使用 Google 登录",
|
||||
backgroundColor = Color(0xFFDB4437),
|
||||
alpha = googleButtonAlpha.value,
|
||||
onClick = { viewModel.handleIntent(AuthIntent.LoginWithGoogle) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AnimatedLoginButton(
|
||||
// Facebook 登录按钮
|
||||
LoginButton(
|
||||
icon = Icons.Default.Facebook,
|
||||
text = "使用 Facebook 登录",
|
||||
backgroundColor = Color(0xFF1877F2),
|
||||
alpha = facebookButtonAlpha.value,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, CategoryIntent, CategoryEffect>(CategoryState()) {
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<OnboardingState, OnboardingIntent, OnboardingEffect>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一页
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, FeedbackIntent, FeedbackEffect>(FeedbackState()) {
|
||||
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
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)
|
||||
// 用户信息卡片(登录前/后通用组件)
|
||||
UserInfoCard(
|
||||
isLoggedIn = true,
|
||||
userName = "DevTTL",
|
||||
userSubtitle = "111111",
|
||||
onClick = { navController.navigate(Routes.Main.Settings.Login) }
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().height(80.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, SettingsIntent, SettingsEffect>(SettingsState()) {
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -16,7 +16,12 @@ data class SplashState(
|
||||
) : BaseState()
|
||||
|
||||
sealed class SplashIntent {
|
||||
object LoadApp : SplashIntent()
|
||||
/**
|
||||
* 装载状态
|
||||
* @author DevTTL
|
||||
* @date 2025/11/07
|
||||
*/
|
||||
object LoadingStatus : SplashIntent()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
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, SplashIntent, SplashEffect>(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()
|
||||
LogUtils.e("DevTTL",hasLaunched.toString())
|
||||
withContext(Dispatchers.Main) {
|
||||
if (hasLaunched) {
|
||||
sendEvent(SplashEffect.NavigateToOnboarding)
|
||||
} else {
|
||||
@@ -50,3 +56,4 @@ class SplashViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package com.taskttl.presentation.features.task.list.components
|
||||
// 任务卡片
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<TaskTTLDatabase> { getDatabaseBuilder() }
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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<TaskTTLDatabase>(name = dbFile.absolutePath)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.setQueryCoroutineContext(Dispatchers.IO)
|
||||
.build()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 <reified T : Any> saveObject(key: String, value: T) {
|
||||
}
|
||||
|
||||
actual inline fun <reified T : Any> 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() {
|
||||
}
|
||||
}
|
||||
@@ -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<TaskTTLDatabase> { getDatabaseBuilder() }
|
||||
}
|
||||
@@ -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<TaskTTLDatabase>(
|
||||
name = dbFile.absolutePath,
|
||||
).build()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"}
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user