Compare commits
3 Commits
5f07605a06
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e954d9d3ed | ||
| 9082f15b63 | |||
| 1ac177be8b |
@@ -75,6 +75,13 @@ kotlin {
|
||||
|
||||
// work
|
||||
implementation(libs.androidx.work)
|
||||
|
||||
// 三方登录
|
||||
// implementation(libs.androidx.login.google)
|
||||
implementation(libs.androidx.login.credentials)
|
||||
implementation(libs.androidx.login.credentials.auth)
|
||||
implementation(libs.androidx.login.googleid)
|
||||
implementation(libs.androidx.login.facebook)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(compose.runtime)
|
||||
@@ -193,6 +200,7 @@ dependencies {
|
||||
// add("kspCommonMainMetadata",libs.androidx.room.compiler)
|
||||
// add("kspCommonMain",libs.androidx.room.compiler)
|
||||
// add("kspWasmJs",libs.androidx.room.compiler)
|
||||
add("kspJvm", libs.androidx.room.compiler)
|
||||
add("kspAndroid", libs.androidx.room.compiler)
|
||||
add("kspIosX64", libs.androidx.room.compiler)
|
||||
add("kspIosArm64", libs.androidx.room.compiler)
|
||||
|
||||
Binary file not shown.
@@ -21,7 +21,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
|
||||
@@ -67,7 +69,7 @@
|
||||
android:name=".core.alarm.AlarmReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver">
|
||||
>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="com.taskttl.ALARM_TRIGGER" />
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.taskttl.app.App
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -16,18 +17,25 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
|
||||
// 设置全屏显示
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
// 设置状态栏图标颜色
|
||||
val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
val controller = WindowInsetsControllerCompat(window, window.decorView)
|
||||
// windowInsetsController.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||
// windowInsetsController.systemBarsBehavior =
|
||||
// WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
|
||||
// 显示状态栏和导航栏
|
||||
// controller.show(WindowInsetsCompat.Type.systemBars())
|
||||
|
||||
// 设置系统栏行为:用户上滑时再显示(可选)
|
||||
// controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
|
||||
// 使用系统原生方法检测暗色主题
|
||||
val isDarkTheme = resources.configuration.uiMode and
|
||||
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
windowInsetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||
controller.isAppearanceLightStatusBars = isDarkTheme
|
||||
controller.isAppearanceLightNavigationBars = isDarkTheme
|
||||
|
||||
setContent {
|
||||
App()
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.taskttl.data.di.initKoin
|
||||
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
|
||||
@@ -32,12 +32,12 @@ class MainApplication : Application() {
|
||||
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
_currentActivity = activity
|
||||
_currentActivity = activity
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (_currentActivity == activity) {
|
||||
_currentActivity = null
|
||||
if (_currentActivity == activity) {
|
||||
_currentActivity = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -3,65 +3,41 @@ package com.taskttl.core.alarm
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.taskttl.core.notification.NotificationManager
|
||||
import com.taskttl.core.notification.AppNotificationManager
|
||||
import com.taskttl.core.notification.NotificationPayload
|
||||
import com.taskttl.core.notification.NotificationRepeatType
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AlarmReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
LogUtils.d("AlarmDebug", "Alarm triggered: ${intent.action}")
|
||||
if (intent.action != "com.taskttl.ALARM_TRIGGER") return
|
||||
|
||||
val id = intent.getStringExtra("id") ?: return
|
||||
val pendingResult = goAsync() // 延长广播生命周期
|
||||
val id = intent.getStringExtra("id") ?: run { pendingResult.finish(); return }
|
||||
val title = intent.getStringExtra("title") ?: "TaskTTL"
|
||||
val message = intent.getStringExtra("message") ?: ""
|
||||
|
||||
// 使用协程调用统一通知方法
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
NotificationManager.scheduleNotification(
|
||||
NotificationPayload(
|
||||
id = id,
|
||||
title = title,
|
||||
message = message,
|
||||
triggerTimeMillis = System.currentTimeMillis(),
|
||||
repeatType = NotificationRepeatType.NONE
|
||||
try {
|
||||
AppNotificationManager.showImmediateNotification(
|
||||
context.applicationContext,
|
||||
NotificationPayload(
|
||||
id = id,
|
||||
title = title,
|
||||
message = message,
|
||||
triggerTimeMillis = System.currentTimeMillis(),
|
||||
repeatType = NotificationRepeatType.NONE
|
||||
)
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
pendingResult.finish() // 完成广播
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// val id = intent.getStringExtra("id") ?: return
|
||||
// val title = intent.getStringExtra("title") ?: "TaskTTL"
|
||||
// val message = intent.getStringExtra("message") ?: ""
|
||||
//
|
||||
// val channelId = "taskttl_channel"
|
||||
//
|
||||
// // 确保通道存在
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
// if (nm.getNotificationChannel(channelId) == null) {
|
||||
// val channel = android.app.NotificationChannel(
|
||||
// channelId,
|
||||
// "TaskTTL Notifications",
|
||||
// android.app.NotificationManager.IMPORTANCE_HIGH
|
||||
// )
|
||||
// channel.description = "TaskTTL 提醒通知"
|
||||
// nm.createNotificationChannel(channel)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 构建通知
|
||||
// val builder = NotificationCompat.Builder(context, channelId)
|
||||
// .setContentTitle(title)
|
||||
// .setContentText(message)
|
||||
// .setSmallIcon(R.mipmap.ic_launcher)
|
||||
// .setAutoCancel(true)
|
||||
// .setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
//
|
||||
// val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
// nm.notify(id.hashCode(), builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.core.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.database
|
||||
package com.taskttl.core.database
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.taskttl.core.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlarmManager
|
||||
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.core.app.NotificationCompat
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
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.utils.LogUtils
|
||||
import com.taskttl.data.constant.Constant
|
||||
import java.util.concurrent.TimeUnit
|
||||
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 const val ONE_DAY_MS = 24 * 60 * 60 * 1000L
|
||||
|
||||
init {
|
||||
setupNotificationChannel()
|
||||
}
|
||||
|
||||
/** 创建通知通道(Android 8+) */
|
||||
private fun setupNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (notificationManager.getNotificationChannel(Constant.CHANNEL_ID) == null) {
|
||||
val channel = android.app.NotificationChannel(
|
||||
Constant.CHANNEL_ID,
|
||||
"TaskTTL Notifications",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 延迟时间计算 */
|
||||
private fun NotificationPayload.delayMillis(): Long =
|
||||
max(0, triggerTimeMillis - System.currentTimeMillis())
|
||||
|
||||
/** 调度通知,安全可后台 */
|
||||
actual suspend fun scheduleNotification(payload: NotificationPayload) {
|
||||
val delay = payload.delayMillis()
|
||||
|
||||
when {
|
||||
delay == 0L -> {
|
||||
LogUtils.d("AlarmDebug", "Showing immediate notification for id=${payload.id}")
|
||||
showImmediateNotification(context, payload)
|
||||
}
|
||||
|
||||
delay <= ONE_DAY_MS && payload.repeatType == NotificationRepeatType.NONE -> {
|
||||
LogUtils.d("AlarmDebug", "Scheduling exact AlarmManager for id=${payload.id}")
|
||||
scheduleExactAlarm(context, payload)
|
||||
}
|
||||
|
||||
else -> {
|
||||
LogUtils.d(
|
||||
"AlarmDebug",
|
||||
"Scheduling WorkManager for id=${payload.id}, repeat=${payload.repeatType}"
|
||||
)
|
||||
scheduleWorkManager(context, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** AlarmManager 精确闹钟 */
|
||||
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()) {
|
||||
LogUtils.w(
|
||||
"AlarmDebug",
|
||||
"Cannot schedule exact alarms, falling back to WorkManager for id=${payload.id}"
|
||||
)
|
||||
scheduleWorkManager(context, payload)
|
||||
return
|
||||
}
|
||||
|
||||
val pendingIntent = createAlarmPendingIntent(context, payload)
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
payload.triggerTimeMillis,
|
||||
pendingIntent
|
||||
)
|
||||
LogUtils.d("AlarmDebug", "Alarm set for id=${payload.id} time=${payload.triggerTimeMillis}")
|
||||
}
|
||||
|
||||
/** 创建 Alarm PendingIntent */
|
||||
private fun createAlarmPendingIntent(
|
||||
context: Context,
|
||||
payload: NotificationPayload,
|
||||
): PendingIntent {
|
||||
val intent = Intent(context, AlarmReceiver::class.java).apply {
|
||||
action = "com.taskttl.ALARM_TRIGGER"
|
||||
putExtra("id", payload.id)
|
||||
putExtra("title", payload.title)
|
||||
putExtra("message", payload.message)
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
payload.id.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
/** WorkManager 长期/周期通知 */
|
||||
private fun scheduleWorkManager(context: Context, payload: NotificationPayload) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val data = workDataOf(
|
||||
"id" to payload.id,
|
||||
"title" to payload.title,
|
||||
"message" to payload.message,
|
||||
"repeatType" to payload.repeatType.name
|
||||
)
|
||||
|
||||
if (payload.repeatType == NotificationRepeatType.NONE) {
|
||||
val request = OneTimeWorkRequestBuilder<NotificationWorker>()
|
||||
.setInitialDelay(payload.delayMillis(), TimeUnit.MILLISECONDS)
|
||||
.setInputData(data)
|
||||
.addTag(payload.id)
|
||||
.build()
|
||||
workManager.enqueue(request)
|
||||
} else {
|
||||
val interval = when (payload.repeatType) {
|
||||
NotificationRepeatType.DAILY -> ONE_DAY_MS
|
||||
NotificationRepeatType.WEEKLY -> 7 * ONE_DAY_MS
|
||||
NotificationRepeatType.MONTHLY -> 30 * ONE_DAY_MS
|
||||
else -> ONE_DAY_MS
|
||||
}
|
||||
|
||||
val request =
|
||||
PeriodicWorkRequestBuilder<NotificationWorker>(interval, TimeUnit.MILLISECONDS)
|
||||
.setInputData(data)
|
||||
.addTag(payload.id)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
payload.id,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 立即显示通知 */
|
||||
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)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
|
||||
.build()
|
||||
notificationManager.notify(payload.id.hashCode(), notification)
|
||||
}
|
||||
|
||||
/** 取消通知 */
|
||||
actual suspend fun cancelNotification(id: String) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
workManager.cancelAllWorkByTag(id)
|
||||
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
id.hashCode(),
|
||||
Intent(context, AlarmReceiver::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
alarmManager.cancel(pendingIntent)
|
||||
notificationManager.cancel(id.hashCode())
|
||||
}
|
||||
|
||||
/** 取消所有通知 */
|
||||
actual suspend fun cancelAll() {
|
||||
WorkManager.getInstance(context).cancelAllWork()
|
||||
notificationManager.cancelAll()
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package com.taskttl.core.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.*
|
||||
import com.taskttl.MainApplication
|
||||
import com.taskttl.R
|
||||
import com.taskttl.core.alarm.AlarmReceiver
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
|
||||
actual object NotificationManager {
|
||||
|
||||
private val channelId = "taskttl_channel"
|
||||
|
||||
init {
|
||||
setupNotificationChannel()
|
||||
}
|
||||
|
||||
private fun getActivity(): Activity = MainApplication.currentActivity()
|
||||
?: throw IllegalStateException("No activity available")
|
||||
|
||||
/** 创建通知通道(Android 8+) */
|
||||
private fun setupNotificationChannel() {
|
||||
val activity = MainApplication.currentActivity() ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
if (nm.getNotificationChannel(channelId) == null) {
|
||||
val channel = android.app.NotificationChannel(
|
||||
channelId,
|
||||
"TaskTTL Notifications",
|
||||
android.app.NotificationManager.IMPORTANCE_HIGH
|
||||
).apply { description = "TaskTTL 提醒通知" }
|
||||
nm.createNotificationChannel(channel)
|
||||
LogUtils.d("DevTTL_NotificationTest", "Notification channel created")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 调度通知 */
|
||||
actual suspend fun scheduleNotification(payload: NotificationPayload) {
|
||||
val activity = getActivity()
|
||||
|
||||
// Android 13+ 权限检查
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1001
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val delay = max(0, payload.triggerTimeMillis - System.currentTimeMillis())
|
||||
if (delay == 0L) {
|
||||
showImmediateNotification(payload)
|
||||
return
|
||||
}
|
||||
|
||||
if (delay <= 24 * 60 * 60 * 1000L && payload.repeatType == NotificationRepeatType.NONE) {
|
||||
scheduleExactAlarm(payload)
|
||||
} else {
|
||||
scheduleWorkManager(payload)
|
||||
}
|
||||
}
|
||||
|
||||
/** AlarmManager 精确闹钟 */
|
||||
private fun scheduleExactAlarm(payload: NotificationPayload) {
|
||||
val activity = getActivity()
|
||||
val alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
|
||||
scheduleWorkManager(payload) // 回退 WorkManager
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(activity, AlarmReceiver::class.java).apply {
|
||||
action = "com.taskttl.ALARM_TRIGGER"
|
||||
putExtra("id", payload.id)
|
||||
putExtra("title", payload.title)
|
||||
putExtra("message", payload.message)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
activity,
|
||||
payload.id.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
payload.triggerTimeMillis,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
/** WorkManager 长期/周期通知 */
|
||||
private fun scheduleWorkManager(payload: NotificationPayload) {
|
||||
val activity = getActivity()
|
||||
val data = workDataOf(
|
||||
"id" to payload.id,
|
||||
"title" to payload.title,
|
||||
"message" to payload.message,
|
||||
"repeatType" to payload.repeatType.name
|
||||
)
|
||||
|
||||
val workManager = WorkManager.getInstance(activity)
|
||||
|
||||
if (payload.repeatType == NotificationRepeatType.NONE) {
|
||||
val delay = max(0, payload.triggerTimeMillis - System.currentTimeMillis())
|
||||
val request = OneTimeWorkRequestBuilder<NotificationWorker>()
|
||||
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
|
||||
.setInputData(data)
|
||||
.addTag(payload.id)
|
||||
.build()
|
||||
workManager.enqueue(request)
|
||||
} else {
|
||||
val interval = when (payload.repeatType) {
|
||||
NotificationRepeatType.DAILY -> 24 * 60 * 60 * 1000L
|
||||
NotificationRepeatType.WEEKLY -> 7 * 24 * 60 * 60 * 1000L
|
||||
NotificationRepeatType.MONTHLY -> 30 * 24 * 60 * 60 * 1000L
|
||||
else -> 24 * 60 * 60 * 1000L
|
||||
}
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<NotificationWorker>(interval, TimeUnit.MILLISECONDS)
|
||||
.setInputData(data)
|
||||
.addTag(payload.id)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
payload.id,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 立即显示通知 */
|
||||
private fun showImmediateNotification(payload: NotificationPayload) {
|
||||
val activity = getActivity()
|
||||
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
|
||||
val notification = NotificationCompat.Builder(activity, channelId)
|
||||
.setContentTitle(payload.title)
|
||||
.setContentText(payload.message)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
// .setDefaults(NotificationCompat.DEFAULT_SOUND)
|
||||
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
|
||||
.build()
|
||||
|
||||
nm.notify(payload.id.hashCode(), notification)
|
||||
}
|
||||
|
||||
|
||||
/** 取消通知 */
|
||||
actual suspend fun cancelNotification(id: String) {
|
||||
val activity = getActivity()
|
||||
WorkManager.getInstance(activity).cancelAllWorkByTag(id)
|
||||
|
||||
val alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(activity, AlarmReceiver::class.java)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
activity,
|
||||
id.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
alarmManager.cancel(pendingIntent)
|
||||
|
||||
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
nm.cancel(id.hashCode())
|
||||
}
|
||||
|
||||
actual suspend fun cancelAll() {
|
||||
val activity = getActivity()
|
||||
WorkManager.getInstance(activity).cancelAllWork()
|
||||
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
nm.cancelAll()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.taskttl.core.notification
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -48,7 +49,7 @@ class NotificationWorker(
|
||||
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(id.hashCode(), builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.taskttl.core.permission
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.taskttl.MainApplication
|
||||
|
||||
/**
|
||||
* 精确报警权限管理器
|
||||
* @author DevTTL
|
||||
* @date 2025/10/23
|
||||
*/// AndroidMain
|
||||
actual object ExactAlarmPermissionManager {
|
||||
private var appContext = MainApplication.instance.applicationContext
|
||||
|
||||
const val ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM"
|
||||
|
||||
actual fun requestPermission(): Boolean {
|
||||
if (verifyPermission()) return true
|
||||
val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
appContext.startActivity(intent)
|
||||
return false
|
||||
}
|
||||
|
||||
actual fun verifyPermission(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
|
||||
val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
return alarmManager.canScheduleExactAlarms()
|
||||
}
|
||||
|
||||
actual fun disablePermission() {
|
||||
// Android 无法强制撤销权限,通常无法实现
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.notification
|
||||
package com.taskttl.core.permission
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
@@ -8,78 +8,56 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import com.taskttl.MainApplication
|
||||
import androidx.core.content.edit
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
|
||||
/**
|
||||
* 通知权限管理器
|
||||
* @author DevTTL
|
||||
* @date 2025/10/23
|
||||
*/
|
||||
actual object NotificationPermissionManager {
|
||||
private const val REQUEST_CODE = 2025
|
||||
private var callback: NotificationPermissionCallback? = null
|
||||
private const val PREFS_NAME = "app_prefs"
|
||||
private const val PREF_FIRST_REQUEST = "notification_first_request"
|
||||
|
||||
/**
|
||||
* 在 Activity 的 onRequestPermissionsResult 中调用
|
||||
*/
|
||||
fun handlePermissionResult(requestCode: Int, grantResults: IntArray) {
|
||||
if (requestCode == REQUEST_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
callback?.onGranted()
|
||||
} else {
|
||||
callback?.onDenied()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expect/actual 适配:Android 需要 Activity 参数
|
||||
actual fun requestPermission(callback: NotificationPermissionCallback) {
|
||||
actual fun requestPermission(): Boolean {
|
||||
// Android 13 以下无需权限
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
callback.onGranted()
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 33) return true
|
||||
|
||||
val activity = MainApplication.currentActivity()
|
||||
if (activity == null) {
|
||||
Log.w("NotificationPermission", "No current activity found")
|
||||
callback.onDenied()
|
||||
return
|
||||
LogUtils.w("NotificationPermission", "No current activity found")
|
||||
return false
|
||||
}
|
||||
this.callback = callback
|
||||
|
||||
val prefs = activity.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
activity, permission
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
callback.onGranted()
|
||||
if (granted) return true
|
||||
val isFirstRequest = prefs.getBoolean("notification_first_request", true)
|
||||
val shouldShowRationale =
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
||||
|
||||
if (!shouldShowRationale && !isFirstRequest) {
|
||||
openNotificationSettings()
|
||||
} else {
|
||||
val isFirstRequest = prefs.getBoolean("notification_first_request", true)
|
||||
val shouldShowRationale =
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
||||
|
||||
if (!shouldShowRationale && !isFirstRequest) {
|
||||
openNotificationSettings()
|
||||
callback.onDenied()
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(activity, arrayOf(permission), REQUEST_CODE)
|
||||
}
|
||||
ActivityCompat.requestPermissions(activity, arrayOf(permission), REQUEST_CODE)
|
||||
}
|
||||
|
||||
prefs.edit { putBoolean("notification_first_request", false) }
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
actual fun verifyPermission(): Boolean {
|
||||
// Android 13 以下无需权限
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
return true
|
||||
}
|
||||
val activity = MainApplication.currentActivity()
|
||||
if (activity == null) {
|
||||
Log.w("NotificationPermission", "No current activity found")
|
||||
return false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 33) return true
|
||||
val activity = MainApplication.currentActivity() ?: return false
|
||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
activity, permission
|
||||
@@ -115,9 +93,9 @@ actual object NotificationPermissionManager {
|
||||
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
Log.d("NotificationPermission", "打开通知设置页")
|
||||
LogUtils.d("NotificationPermission", "打开通知设置页")
|
||||
} catch (e: Exception) {
|
||||
Log.e("NotificationPermission", "无法打开通知设置页: ${e.message}")
|
||||
LogUtils.e("NotificationPermission", "无法打开通知设置页: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package com.taskttl.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.telephony.TelephonyManager
|
||||
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
||||
import com.taskttl.BuildConfig
|
||||
import com.taskttl.MainApplication
|
||||
@@ -136,4 +138,13 @@ actual object DeviceUtils {
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
fun getSimOrNetworkCountry(): String {
|
||||
val tm = appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||
return when {
|
||||
!tm.networkCountryIso.isNullOrEmpty() -> tm.networkCountryIso.uppercase()
|
||||
!tm.simCountryIso.isNullOrEmpty() -> tm.simCountryIso.uppercase()
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ actual object ExternalAppLauncher {
|
||||
// 如果前面的方法都失败,直接打开网页版
|
||||
openWebUrl(webUrl)
|
||||
} catch (e: Exception) {
|
||||
// Log.e(TAG, getString(Res.string.external_app_launcher_open_rating_failed), e)
|
||||
// LogUtils.w(TAG, getString(Res.string.external_app_launcher_open_rating_failed), e)
|
||||
openWebUrl(webUrl)
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ actual object ExternalAppLauncher {
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log.w(TAG, getString(Res.string.external_app_launcher_open_uri_failed, uri), e)
|
||||
// LogUtils.w(TAG, getString(Res.string.external_app_launcher_open_uri_failed, uri), e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -65,7 +65,7 @@ actual object ExternalAppLauncher {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
appContext.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
// Log.e(TAG, getString(Res.string.external_app_launcher_open_web_failed, url), e)
|
||||
// LogUtils.w(TAG, getString(Res.string.external_app_launcher_open_web_failed, url), e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +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 fun platformModule() = module {
|
||||
single<TaskTTLDatabase> { getDatabaseBuilder() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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,9 @@
|
||||
{
|
||||
"installed": {
|
||||
"client_id": "649192447921-2rqgmislv7mbt2lgib0u422c43cuph61.apps.googleusercontent.com",
|
||||
"project_id": "taskttl-476406",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -313,4 +313,6 @@
|
||||
<string name="feedback_success">反馈成功</string>
|
||||
<string name="feedback_error">反馈失败,请检查网络连接或稍后重试</string>
|
||||
|
||||
<string name="task_due_format">yyyy年MM月dd日 EEEE HH:mm</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -314,4 +314,6 @@
|
||||
<string name="feedback_success">Feedback successful</string>
|
||||
<string name="feedback_error">Feedback failed. Please check your network connection or try again later.</string>
|
||||
|
||||
<string name="task_due_format">EEE, MMM d, yyyy h:mm a</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.taskttl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.taskttl.core.routes.AppNav
|
||||
import com.taskttl.ui.theme.AppTheme
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
|
||||
/**
|
||||
* 应用
|
||||
*/
|
||||
@Preview
|
||||
@Composable
|
||||
fun App() {
|
||||
AppTheme {
|
||||
AppNav()
|
||||
}
|
||||
}
|
||||
35
composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt
Normal file
35
composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package com.taskttl.app
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.taskttl.core.manager.ThemeMode
|
||||
import com.taskttl.navigation.AppNav
|
||||
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()) {
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
// 获取系统主题设置
|
||||
val systemDarkTheme = isSystemInDarkTheme()
|
||||
|
||||
val isDarkTheme = when (state.themeMode) {
|
||||
ThemeMode.LIGHT -> false
|
||||
ThemeMode.DARK -> true
|
||||
ThemeMode.SYSTEM -> systemDarkTheme
|
||||
}
|
||||
|
||||
AppTheme(darkTheme = isDarkTheme) {
|
||||
AppNav()
|
||||
}
|
||||
}
|
||||
29
composeApp/src/commonMain/kotlin/com/taskttl/app/AppState.kt
Normal file
29
composeApp/src/commonMain/kotlin/com/taskttl/app/AppState.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.taskttl.app
|
||||
|
||||
import com.taskttl.core.base.BaseState
|
||||
import com.taskttl.core.manager.ThemeMode
|
||||
|
||||
data class AppState(
|
||||
override val isLoading: Boolean = false,
|
||||
override val isProcessing: Boolean = false,
|
||||
override val error: String? = null,
|
||||
val themeMode: ThemeMode = ThemeMode.SYSTEM,
|
||||
) : BaseState()
|
||||
|
||||
/**
|
||||
* 应用程序意图
|
||||
* @author DevTTL
|
||||
* @date 2025/10/28
|
||||
* @constructor 创建[AppIntent]
|
||||
*/
|
||||
sealed class AppIntent {
|
||||
|
||||
/**
|
||||
* 加载主题
|
||||
* @author DevTTL
|
||||
* @date 2025/10/28
|
||||
*/
|
||||
object LoadTheme : AppIntent()
|
||||
}
|
||||
|
||||
sealed class AppEffect {}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.taskttl.app
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.taskttl.core.base.BaseViewModel
|
||||
import com.taskttl.core.manager.ThemeManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 应用程序视图模型
|
||||
* @author DevTTL
|
||||
* @date 2025/10/28
|
||||
* @constructor 创建[AppViewModel]
|
||||
* @param [themeManager] 主题管理器
|
||||
*/
|
||||
class AppViewModel(private val themeManager: ThemeManager) :
|
||||
BaseViewModel<AppState, AppIntent, AppEffect>(AppState()) {
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
themeManager.themeMode.collect { mode -> updateState { copy(themeMode = mode) } }
|
||||
}
|
||||
// handleIntent(AppIntent.LoadTheme)
|
||||
}
|
||||
|
||||
public override fun handleIntent(intent: AppIntent) {
|
||||
when (intent) {
|
||||
is AppIntent.LoadTheme -> loadTheme()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTheme() {
|
||||
viewModelScope.launch {
|
||||
updateState { copy(themeMode = themeManager.themeMode.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.taskttl.app.di
|
||||
|
||||
import org.koin.core.module.Module
|
||||
|
||||
expect val serviceModule: Module
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.taskttl.data.di
|
||||
package com.taskttl.app.di
|
||||
|
||||
import com.taskttl.data.local.database.TaskTTLDatabase
|
||||
import com.taskttl.data.local.database.getDatabaseBuilder
|
||||
|
||||
import com.taskttl.core.database.TaskTTLDatabase
|
||||
import com.taskttl.core.database.getDatabaseBuilder
|
||||
import com.taskttl.data.mapper.CategoryMapper
|
||||
import com.taskttl.data.mapper.CountdownMapper
|
||||
import com.taskttl.data.mapper.TaskMapper
|
||||
import com.taskttl.data.repository.impl.CategoryRepositoryImpl
|
||||
import com.taskttl.data.repository.impl.CountdownRepositoryImpl
|
||||
import com.taskttl.data.repository.impl.TaskRepositoryImpl
|
||||
import com.taskttl.data.repository.CategoryRepository
|
||||
import com.taskttl.data.repository.CountdownRepository
|
||||
import com.taskttl.data.repository.TaskRepository
|
||||
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.module
|
||||
|
||||
|
||||
@@ -29,4 +32,6 @@ val dataModule = module {
|
||||
single<TaskRepository> { TaskRepositoryImpl(get(), get()) }
|
||||
single<CountdownRepository> { CountdownRepositoryImpl(get(), get()) }
|
||||
single<CategoryRepository> { CategoryRepositoryImpl(get(), get(), get(), get()) }
|
||||
|
||||
singleOf(::AuthRepository)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.taskttl.app.di
|
||||
|
||||
import com.taskttl.app.AppViewModel
|
||||
import com.taskttl.core.manager.ThemeManager
|
||||
import com.taskttl.data.repository.OnboardingRepositoryImpl
|
||||
import com.taskttl.data.repository.SettingsRepositoryImpl
|
||||
import com.taskttl.domain.repository.OnboardingRepository
|
||||
import com.taskttl.domain.repository.SettingsRepository
|
||||
import com.taskttl.presentation.features.auth.AuthViewModel
|
||||
import com.taskttl.presentation.features.category.list.CategoryViewModel
|
||||
import com.taskttl.presentation.features.countdown.list.CountdownViewModel
|
||||
import com.taskttl.presentation.features.onboarding.OnboardingViewModel
|
||||
import com.taskttl.presentation.features.settings.feedback.FeedbackViewModel
|
||||
import com.taskttl.presentation.features.settings.main.SettingsViewModel
|
||||
import com.taskttl.presentation.features.splash.SplashViewModel
|
||||
import com.taskttl.presentation.features.task.list.TaskViewModel
|
||||
import org.koin.core.KoinApplication
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
/**
|
||||
* 初始化Koin
|
||||
* @param [config] 配置
|
||||
*/
|
||||
fun initKoin(config: (KoinApplication.() -> Unit)? = null) {
|
||||
startKoin {
|
||||
config?.invoke(this)
|
||||
modules(repositoryModule, viewModelModule, serviceModule, dataModule)
|
||||
}
|
||||
}
|
||||
|
||||
/** 存储库模块 */
|
||||
val repositoryModule = module {
|
||||
singleOf(::ThemeManager)
|
||||
singleOf(::OnboardingRepositoryImpl).bind(OnboardingRepository::class)
|
||||
singleOf(::SettingsRepositoryImpl).bind(SettingsRepository::class)
|
||||
}
|
||||
|
||||
/** 视图模型模块 */
|
||||
val viewModelModule = module {
|
||||
|
||||
viewModelOf(::AppViewModel)
|
||||
viewModelOf(::SplashViewModel)
|
||||
viewModelOf(::OnboardingViewModel)
|
||||
viewModelOf(::TaskViewModel)
|
||||
viewModelOf(::CategoryViewModel)
|
||||
viewModelOf(::CountdownViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::FeedbackViewModel)
|
||||
viewModelOf(::AuthViewModel)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.taskttl.core.base
|
||||
|
||||
/**
|
||||
* 基本ui状态
|
||||
* @author DevTTL
|
||||
* @date 2025/10/15
|
||||
* @constructor 创建[BaseState]
|
||||
* @param [isLoading] 正在加载
|
||||
* @param [isProcessing] 正在处理
|
||||
* @param [error] 错误
|
||||
*/
|
||||
open class BaseState(
|
||||
open val isLoading: Boolean = false,
|
||||
open val isProcessing: Boolean = false,
|
||||
open val error: String? = null,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.viewmodel
|
||||
package com.taskttl.core.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
|
||||
* @constructor 创建[BaseViewModel]
|
||||
* @param [initialState] 初始状态
|
||||
*/
|
||||
abstract class BaseViewModel<S : BaseUiState, I, E>(initialState: S) : ViewModel() {
|
||||
abstract class BaseViewModel<S : BaseState, I, E>(initialState: S) : ViewModel() {
|
||||
|
||||
// 状态流
|
||||
private val _state = MutableStateFlow(initialState)
|
||||
@@ -81,17 +81,3 @@ abstract class BaseViewModel<S : BaseUiState, I, E>(initialState: S) : ViewModel
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 基本ui状态
|
||||
* @author DevTTL
|
||||
* @date 2025/10/15
|
||||
* @constructor 创建[BaseUiState]
|
||||
* @param [isLoading] 正在加载
|
||||
* @param [isProcessing] 正在处理
|
||||
* @param [error] 错误
|
||||
*/
|
||||
open class BaseUiState(
|
||||
open val isLoading: Boolean = false,
|
||||
open val isProcessing: Boolean = false,
|
||||
open val error: String? = null,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.core.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.taskttl.data.local.database
|
||||
package com.taskttl.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.taskttl.data.local.dao.CategoryDao
|
||||
import com.taskttl.data.local.dao.CountdownDao
|
||||
import com.taskttl.data.local.dao.TaskDao
|
||||
import com.taskttl.data.local.entity.CategoryEntity
|
||||
import com.taskttl.data.local.entity.CountdownEntity
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import com.taskttl.data.source.local.dao.CategoryDao
|
||||
import com.taskttl.data.source.local.dao.CountdownDao
|
||||
import com.taskttl.data.source.local.dao.TaskDao
|
||||
import com.taskttl.data.source.local.entity.CategoryEntity
|
||||
import com.taskttl.data.source.local.entity.CountdownEntity
|
||||
import com.taskttl.data.source.local.entity.TaskEntity
|
||||
|
||||
/**
|
||||
* TaskTTL数据库
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.taskttl.core.manager
|
||||
|
||||
import com.taskttl.core.utils.StorageUtils
|
||||
import com.taskttl.data.constant.Constant
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* 主题管理器
|
||||
* @author DevTTL
|
||||
* @date 2025/10/28
|
||||
* @constructor 创建[ThemeManager]
|
||||
*/
|
||||
class ThemeManager() {
|
||||
private val _themeMode = MutableStateFlow(ThemeMode.SYSTEM)
|
||||
val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow()
|
||||
|
||||
init {
|
||||
// 从本地存储加载主题设置
|
||||
loadThemeFromPreferences()
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储加载主题
|
||||
*/
|
||||
private fun loadThemeFromPreferences() {
|
||||
val modeString = StorageUtils.getString(Constant.KEY_DARK_MODE, ThemeMode.SYSTEM.name)
|
||||
_themeMode.value = ThemeMode.fromName(modeString)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题模式
|
||||
*/
|
||||
fun setDarkTheme(isDark: ThemeMode) {
|
||||
_themeMode.value = isDark
|
||||
// 保存到本地存储
|
||||
StorageUtils.saveString(Constant.KEY_DARK_MODE, isDark.name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.taskttl.core.manager
|
||||
|
||||
/**
|
||||
* 主题模式枚举
|
||||
*/
|
||||
enum class ThemeMode {
|
||||
LIGHT, // 明亮模式
|
||||
DARK, // 暗黑模式
|
||||
SYSTEM; // 跟随系统
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String?): ThemeMode {
|
||||
return when (name?.uppercase()) {
|
||||
"LIGHT" -> LIGHT
|
||||
"DARK" -> DARK
|
||||
"SYSTEM" -> SYSTEM
|
||||
else -> SYSTEM // 默认返回 SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ package com.taskttl.core.notification
|
||||
* 通知管理器
|
||||
* @author admin
|
||||
* @date 2025/10/16
|
||||
* @constructor 创建[NotificationManager]
|
||||
* @constructor 创建[AppNotificationManager]
|
||||
*/
|
||||
expect object NotificationManager {
|
||||
expect object AppNotificationManager {
|
||||
/**
|
||||
* 日程通知
|
||||
* @param [payload] 有效载荷
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.taskttl.core.notification
|
||||
|
||||
/**
|
||||
* 通知权限回调接口
|
||||
*/
|
||||
interface NotificationPermissionCallback {
|
||||
/**
|
||||
* 授予
|
||||
*/
|
||||
fun onGranted()
|
||||
|
||||
/**
|
||||
* 被否认
|
||||
*/
|
||||
fun onDenied()
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨平台通知权限处理器
|
||||
*/
|
||||
expect object NotificationPermissionManager {
|
||||
/**
|
||||
* 请求通知权限
|
||||
* @return 是否允许
|
||||
*/
|
||||
fun requestPermission(callback: NotificationPermissionCallback)
|
||||
|
||||
fun verifyPermission(): Boolean
|
||||
|
||||
fun disablePermission()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.taskttl.core.permission
|
||||
|
||||
/**
|
||||
* 跨平台精确闹钟权限处理器
|
||||
*/
|
||||
expect object ExactAlarmPermissionManager {
|
||||
|
||||
/**
|
||||
* 请求精确闹钟权限
|
||||
* Android 12+ 需要用户允许
|
||||
* iOS/其他平台无需操作,直接回调 true
|
||||
*
|
||||
*/
|
||||
fun requestPermission(): Boolean
|
||||
|
||||
/**
|
||||
* 验证当前是否有精确闹钟权限
|
||||
*/
|
||||
fun verifyPermission(): Boolean
|
||||
|
||||
/**
|
||||
* 禁用或撤销权限(如果平台支持)
|
||||
*/
|
||||
fun disablePermission()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.taskttl.core.permission
|
||||
|
||||
|
||||
/**
|
||||
* 跨平台通知权限处理器
|
||||
*/
|
||||
expect object NotificationPermissionManager {
|
||||
/**
|
||||
* 请求通知权限
|
||||
* @return 是否允许
|
||||
*/
|
||||
fun requestPermission(): Boolean
|
||||
|
||||
fun verifyPermission(): Boolean
|
||||
|
||||
fun disablePermission()
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
package com.taskttl.core.ui
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.FloatingActionButtonElevation
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import androidx.compose.ui.util.fastMapIndexed
|
||||
|
||||
|
||||
/**
|
||||
* Note: fabeSize maintains the same height of modifier
|
||||
*/
|
||||
@Composable
|
||||
fun NotchedBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
icons: List<ImageVector>,
|
||||
selectedIndex: Int,
|
||||
fabIcon: ImageVector,
|
||||
fabIconSize: Dp = 28.dp,
|
||||
onIconClick: (index: Int) -> Unit,
|
||||
onFabClick: () -> Unit,
|
||||
fabSize: Dp,
|
||||
fabColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||
fabContainerColor: Color = contentColorFor(fabColor),
|
||||
fabElevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||
iconColor: Color = NavigationBarItemDefaults.colors().unselectedIconColor,
|
||||
selectedIconColor: Color = NavigationBarItemDefaults.colors().selectedIconColor,
|
||||
iconContainerColor: Color = NavigationBarItemDefaults.colors().selectedIndicatorColor,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
SubcomposeLayout(
|
||||
modifier = modifier
|
||||
) { constraints ->
|
||||
// measure fab
|
||||
val fabPlaceables = subcompose("fab") {
|
||||
FloatingActionButton(
|
||||
onClick = onFabClick,
|
||||
modifier = Modifier.size(fabSize),
|
||||
containerColor = fabContainerColor,
|
||||
contentColor = fabColor,
|
||||
elevation = fabElevation,
|
||||
shape = CircleShape
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(fabIconSize),
|
||||
imageVector = fabIcon,
|
||||
contentDescription = "fab",
|
||||
tint = fabColor
|
||||
)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val fabWidth = fabPlaceables.maxOf { it.width }
|
||||
val fabHeight = fabPlaceables.maxOf { it.height }
|
||||
|
||||
// measure icons
|
||||
val iconPlaceables = icons.fastMapIndexed { index, icon ->
|
||||
val measurable = subcompose("icon_$index") {
|
||||
val selected = selectedIndex == index
|
||||
Surface(
|
||||
onClick = { onIconClick(index) },
|
||||
shape = CircleShape,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.aspectRatio(16 / 9f),
|
||||
color = if (selected) iconContainerColor else Color.Transparent,
|
||||
contentColor = if (selected) selectedIconColor else iconColor
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "$index-icon",
|
||||
tint = if (selected) selectedIconColor else iconColor
|
||||
)
|
||||
}
|
||||
}.first().measure(constraints)
|
||||
measurable
|
||||
}
|
||||
|
||||
val canvasHeight = iconPlaceables.maxOf { it.height }
|
||||
|
||||
// measure background
|
||||
val backgroundPlaceables = subcompose("background") {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(with(density) { canvasHeight.toDp() })
|
||||
) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
val fabRadius = fabWidth / 2f
|
||||
val centerX = width / 2f
|
||||
val curveDepth = fabHeight * 0.6f
|
||||
val controlOffsetX = fabWidth.toFloat()
|
||||
|
||||
val path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(centerX - fabRadius - controlOffsetX, 0f)
|
||||
cubicTo(
|
||||
centerX - fabRadius, 0f,
|
||||
centerX - fabRadius, curveDepth,
|
||||
centerX, curveDepth
|
||||
)
|
||||
cubicTo(
|
||||
centerX + fabRadius, curveDepth,
|
||||
centerX + fabRadius, 0f,
|
||||
centerX + fabRadius + controlOffsetX, 0f
|
||||
)
|
||||
lineTo(width, 0f)
|
||||
lineTo(width, height)
|
||||
lineTo(0f, height)
|
||||
close()
|
||||
}
|
||||
drawPath(path, color = containerColor)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
// calculate icon space
|
||||
val totalItemsWidth = iconPlaceables.sumOf { it.width } + fabWidth
|
||||
val spaceCount = icons.size + 1
|
||||
val spaceWidth = (constraints.maxWidth - totalItemsWidth).coerceAtLeast(0) / spaceCount
|
||||
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
backgroundPlaceables.forEach {
|
||||
it.place(0, constraints.maxHeight - it.height)
|
||||
}
|
||||
|
||||
var x = spaceWidth
|
||||
iconPlaceables.fastForEachIndexed { index, it ->
|
||||
it.place(x, 0)
|
||||
x += it.width + if (index == (icons.size / 2 - 1)) {
|
||||
spaceWidth + fabWidth // keep the space for fab
|
||||
} else {
|
||||
spaceWidth
|
||||
}
|
||||
}
|
||||
|
||||
fabPlaceables.forEach {
|
||||
it.place(
|
||||
(constraints.maxWidth - it.width) / 2,
|
||||
0 - fabHeight / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Note: fabeSize maintains the same height of modifier
|
||||
*/
|
||||
@Composable
|
||||
fun NotchedBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
icons: List<Int>,
|
||||
selectedIndex: Int,
|
||||
@DrawableRes fabIcon: Int,
|
||||
fabIconSize: Dp = 28.dp,
|
||||
onIconClick: (index: Int) -> Unit,
|
||||
onFabClick: () -> Unit,
|
||||
fabSize: Dp,
|
||||
fabColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||
fabContainerColor: Color = contentColorFor(fabColor),
|
||||
fabElevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||
iconColor: Color = NavigationBarItemDefaults.colors().unselectedIconColor,
|
||||
selectedIconColor: Color = NavigationBarItemDefaults.colors().selectedIconColor,
|
||||
iconContainerColor: Color = NavigationBarItemDefaults.colors().selectedIndicatorColor,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
SubcomposeLayout(
|
||||
modifier = modifier
|
||||
) { constraints ->
|
||||
// measure fab
|
||||
val fabPlaceables = subcompose("fab") {
|
||||
FloatingActionButton(
|
||||
onClick = onFabClick,
|
||||
modifier = Modifier.size(fabSize),
|
||||
containerColor = fabContainerColor,
|
||||
contentColor = fabColor,
|
||||
elevation = fabElevation,
|
||||
shape = CircleShape
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(fabIconSize),
|
||||
painter = painterResource(fabIcon),
|
||||
contentDescription = "fab",
|
||||
colorFilter = ColorFilter.tint(fabColor)
|
||||
)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val fabWidth = fabPlaceables.maxOf { it.width }
|
||||
val fabHeight = fabPlaceables.maxOf { it.height }
|
||||
|
||||
// measure icons
|
||||
val iconPlaceables = icons.fastMapIndexed { index, icon ->
|
||||
val measurable = subcompose("icon_$index") {
|
||||
val selected = selectedIndex == index
|
||||
Surface(
|
||||
onClick = { onIconClick(index) },
|
||||
shape = CircleShape,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.aspectRatio(16 / 9f),
|
||||
color = if (selected) iconContainerColor else Color.Transparent,
|
||||
contentColor = if (selected) selectedIconColor else iconColor
|
||||
) {
|
||||
Image(
|
||||
modifier= Modifier
|
||||
.wrapContentSize().padding(vertical = 3.dp),
|
||||
painter = painterResource(icon),
|
||||
contentDescription = "$index-icon",
|
||||
colorFilter = ColorFilter.tint(iconColor)
|
||||
)
|
||||
}
|
||||
}.first().measure(constraints)
|
||||
measurable
|
||||
}
|
||||
|
||||
val canvasHeight = iconPlaceables.maxOf { it.height }
|
||||
|
||||
// measure background
|
||||
val backgroundPlaceables = subcompose("background") {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(with(density) { canvasHeight.toDp() })
|
||||
) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
val fabRadius = fabWidth / 2f
|
||||
val centerX = width / 2f
|
||||
val curveDepth = fabHeight * 0.6f
|
||||
val controlOffsetX = fabWidth.toFloat()
|
||||
|
||||
val path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(centerX - fabRadius - controlOffsetX, 0f)
|
||||
cubicTo(
|
||||
centerX - fabRadius, 0f,
|
||||
centerX - fabRadius, curveDepth,
|
||||
centerX, curveDepth
|
||||
)
|
||||
cubicTo(
|
||||
centerX + fabRadius, curveDepth,
|
||||
centerX + fabRadius, 0f,
|
||||
centerX + fabRadius + controlOffsetX, 0f
|
||||
)
|
||||
lineTo(width, 0f)
|
||||
lineTo(width, height)
|
||||
lineTo(0f, height)
|
||||
close()
|
||||
}
|
||||
drawPath(path, color = containerColor)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
// calculate icon space
|
||||
val totalItemsWidth = iconPlaceables.sumOf { it.width } + fabWidth
|
||||
val spaceCount = icons.size + 1
|
||||
val spaceWidth = (constraints.maxWidth - totalItemsWidth).coerceAtLeast(0) / spaceCount
|
||||
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
backgroundPlaceables.forEach {
|
||||
it.place(0, constraints.maxHeight - it.height)
|
||||
}
|
||||
|
||||
var x = spaceWidth
|
||||
iconPlaceables.fastForEachIndexed { index, it ->
|
||||
it.place(x, 0)
|
||||
x += it.width + if (index == (icons.size / 2 - 1)) {
|
||||
spaceWidth + fabWidth // keep the space for fab
|
||||
} else {
|
||||
spaceWidth
|
||||
}
|
||||
}
|
||||
|
||||
fabPlaceables.forEach {
|
||||
it.place(
|
||||
(constraints.maxWidth - it.width) / 2,
|
||||
0 - fabHeight / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.taskttl.core.utils
|
||||
|
||||
import com.taskttl.data.local.model.CountdownTime
|
||||
import com.taskttl.domain.model.CountdownTime
|
||||
import kotlinx.datetime.DatePeriod
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
@@ -22,4 +22,4 @@ expect object DeviceUtils {
|
||||
*/
|
||||
suspend fun getDeviceInfo(): BaseReq
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.taskttl.data.constant
|
||||
|
||||
/**
|
||||
* 常量
|
||||
* @author DevTTL
|
||||
* @date 2025/10/24
|
||||
*/
|
||||
object Constant {
|
||||
/** 频道id */
|
||||
const val CHANNEL_ID = "taskttl_channel"
|
||||
|
||||
/** 按键暗模式 */
|
||||
const val KEY_DARK_MODE = "dark_mode"
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.taskttl.data.di
|
||||
|
||||
import com.taskttl.core.notification.NotificationManager
|
||||
import com.taskttl.data.repository.OnboardingRepository
|
||||
import com.taskttl.data.repository.SettingsRepository
|
||||
import com.taskttl.data.repository.impl.OnboardingRepositoryImpl
|
||||
import com.taskttl.data.repository.impl.SettingsRepositoryImpl
|
||||
import com.taskttl.data.viewmodel.CategoryViewModel
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
import com.taskttl.data.viewmodel.FeedbackViewModel
|
||||
import com.taskttl.data.viewmodel.OnboardingViewModel
|
||||
import com.taskttl.data.viewmodel.SettingsViewModel
|
||||
import com.taskttl.data.viewmodel.SplashViewModel
|
||||
import com.taskttl.data.viewmodel.TaskViewModel
|
||||
import org.koin.core.KoinApplication
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
expect fun platformModule(): Module
|
||||
|
||||
/**
|
||||
* 初始化Koin
|
||||
* @param [config] 配置
|
||||
*/
|
||||
fun initKoin(config: (KoinApplication.() -> Unit)? = null) {
|
||||
startKoin {
|
||||
config?.invoke(this)
|
||||
modules(repositoryModule, viewModelModule, platformModule(), dataModule)
|
||||
}
|
||||
}
|
||||
|
||||
/** 存储库模块 */
|
||||
val repositoryModule = module {
|
||||
singleOf(::OnboardingRepositoryImpl).bind(OnboardingRepository::class)
|
||||
singleOf(::SettingsRepositoryImpl).bind(SettingsRepository::class)
|
||||
}
|
||||
|
||||
/** 视图模型模块 */
|
||||
val viewModelModule = module {
|
||||
viewModelOf(::SplashViewModel)
|
||||
viewModelOf(::OnboardingViewModel)
|
||||
viewModelOf(::TaskViewModel)
|
||||
viewModelOf(::CategoryViewModel)
|
||||
viewModelOf(::CountdownViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::FeedbackViewModel)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.taskttl.data.mapper
|
||||
|
||||
import com.taskttl.data.local.entity.CategoryEntity
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryColor
|
||||
import com.taskttl.data.local.model.CategoryIcon
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.source.local.entity.CategoryEntity
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryColor
|
||||
import com.taskttl.domain.model.CategoryIcon
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.taskttl.data.mapper
|
||||
|
||||
import com.taskttl.data.local.entity.CountdownEntity
|
||||
import com.taskttl.data.local.entity.CountdownWithCategory
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryColor
|
||||
import com.taskttl.data.local.model.CategoryIcon
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.data.local.model.ReminderFrequency
|
||||
import com.taskttl.data.source.local.entity.CountdownEntity
|
||||
import com.taskttl.data.source.local.entity.CountdownWithCategory
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryColor
|
||||
import com.taskttl.domain.model.CategoryIcon
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import com.taskttl.domain.model.Countdown
|
||||
import com.taskttl.domain.model.ReminderFrequency
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.taskttl.data.mapper
|
||||
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import com.taskttl.data.local.entity.TaskWithCategory
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryColor
|
||||
import com.taskttl.data.local.model.CategoryIcon
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.local.model.Task
|
||||
import com.taskttl.data.local.model.TaskPriority
|
||||
import com.taskttl.data.source.local.entity.TaskEntity
|
||||
import com.taskttl.data.source.local.entity.TaskWithCategory
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryColor
|
||||
import com.taskttl.domain.model.CategoryIcon
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import com.taskttl.domain.model.Task
|
||||
import com.taskttl.domain.model.TaskPriority
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.taskttl.data.repository.impl
|
||||
package com.taskttl.data.repository
|
||||
|
||||
import com.taskttl.data.local.dao.CategoryDao
|
||||
import com.taskttl.data.local.dao.CountdownDao
|
||||
import com.taskttl.data.local.dao.TaskDao
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryColor
|
||||
import com.taskttl.data.local.model.CategoryIcon
|
||||
import com.taskttl.data.local.model.CategoryStatistics
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.mapper.CategoryMapper
|
||||
import com.taskttl.data.repository.CategoryRepository
|
||||
import com.taskttl.data.source.local.dao.CategoryDao
|
||||
import com.taskttl.data.source.local.dao.CountdownDao
|
||||
import com.taskttl.data.source.local.dao.TaskDao
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryColor
|
||||
import com.taskttl.domain.model.CategoryIcon
|
||||
import com.taskttl.domain.model.CategoryStatistics
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import com.taskttl.domain.repository.CategoryRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.taskttl.data.repository.impl
|
||||
package com.taskttl.data.repository
|
||||
|
||||
import com.taskttl.data.local.dao.CountdownDao
|
||||
import com.taskttl.data.source.local.dao.CountdownDao
|
||||
import com.taskttl.data.mapper.CountdownMapper
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.data.repository.CountdownRepository
|
||||
import com.taskttl.domain.model.Countdown
|
||||
import com.taskttl.domain.repository.CountdownRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.taskttl.data.repository.impl
|
||||
package com.taskttl.data.repository
|
||||
|
||||
import com.taskttl.core.utils.StorageUtils
|
||||
import com.taskttl.data.repository.OnboardingRepository
|
||||
import com.taskttl.domain.repository.OnboardingRepository
|
||||
|
||||
/**
|
||||
* 设置存储库impl
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.taskttl.data.repository.impl
|
||||
package com.taskttl.data.repository
|
||||
|
||||
import com.taskttl.core.utils.StorageUtils
|
||||
import com.taskttl.data.repository.SettingsRepository
|
||||
import com.taskttl.domain.repository.SettingsRepository
|
||||
|
||||
/**
|
||||
* 设置存储库impl
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.taskttl.data.repository.impl
|
||||
package com.taskttl.data.repository
|
||||
|
||||
import com.taskttl.data.local.dao.TaskDao
|
||||
import com.taskttl.data.source.local.dao.TaskDao
|
||||
import com.taskttl.data.mapper.TaskMapper
|
||||
import com.taskttl.data.local.model.Task
|
||||
import com.taskttl.data.repository.TaskRepository
|
||||
import com.taskttl.domain.model.Task
|
||||
import com.taskttl.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.taskttl.data.local.dao
|
||||
package com.taskttl.data.source.local.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.taskttl.data.local.entity.CategoryEntity
|
||||
import com.taskttl.data.source.local.entity.CategoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.taskttl.data.local.dao
|
||||
package com.taskttl.data.source.local.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.taskttl.data.local.entity.CountdownEntity
|
||||
import com.taskttl.data.local.entity.CountdownWithCategory
|
||||
import com.taskttl.data.source.local.entity.CountdownEntity
|
||||
import com.taskttl.data.source.local.entity.CountdownWithCategory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.dao
|
||||
package com.taskttl.data.source.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
@@ -6,8 +6,8 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import com.taskttl.data.local.entity.TaskWithCategory
|
||||
import com.taskttl.data.source.local.entity.TaskEntity
|
||||
import com.taskttl.data.source.local.entity.TaskWithCategory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.entity
|
||||
package com.taskttl.data.source.local.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.entity
|
||||
package com.taskttl.data.source.local.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.entity
|
||||
package com.taskttl.data.source.local.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.entity
|
||||
package com.taskttl.data.source.local.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.entity
|
||||
package com.taskttl.data.source.local.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.network
|
||||
package com.taskttl.data.source.remote.api
|
||||
|
||||
import com.taskttl.core.domain.constant.PointEvent
|
||||
import com.taskttl.core.domain.toJson
|
||||
@@ -6,9 +6,9 @@ import com.taskttl.core.network.ApiConfig
|
||||
import com.taskttl.core.network.KtorClient
|
||||
import com.taskttl.core.utils.DeviceUtils
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
import com.taskttl.data.network.domain.req.FeedbackReq
|
||||
import com.taskttl.data.network.domain.req.PointReq
|
||||
import com.taskttl.data.network.domain.resp.FeedbackResp
|
||||
import com.taskttl.data.source.remote.dto.request.FeedbackReq
|
||||
import com.taskttl.data.source.remote.dto.request.PointReq
|
||||
import com.taskttl.data.source.remote.dto.response.FeedbackResp
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.feedback_error
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.network.domain.req
|
||||
package com.taskttl.data.source.remote.dto.request
|
||||
|
||||
import com.taskttl.core.domain.BaseReqWith
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.network.domain.req
|
||||
package com.taskttl.data.source.remote.dto.request
|
||||
|
||||
import com.taskttl.core.domain.BaseReqWith
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.taskttl.data.source.remote.dto.response
|
||||
|
||||
sealed class AuthResult {
|
||||
data class Success(val userId: String, val token: String) : AuthResult()
|
||||
data class Error(val message: String) : AuthResult()
|
||||
data object Canceled : AuthResult()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.network.domain.resp
|
||||
package com.taskttl.data.source.remote.dto.response
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.model
|
||||
package com.taskttl.domain.model
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Assignment
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.model
|
||||
package com.taskttl.domain.model
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.model
|
||||
package com.taskttl.domain.model
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.local.model
|
||||
package com.taskttl.domain.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.taskttl.domain.repository
|
||||
|
||||
import com.taskttl.data.source.remote.dto.response.AuthResult
|
||||
|
||||
expect class AuthRepository() {
|
||||
suspend fun loginWithGoogle(): AuthResult
|
||||
suspend fun loginWithFacebook(): AuthResult
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.taskttl.data.repository
|
||||
package com.taskttl.domain.repository
|
||||
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryStatistics
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryStatistics
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.taskttl.data.repository
|
||||
package com.taskttl.domain.repository
|
||||
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.domain.model.Countdown
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.repository
|
||||
package com.taskttl.domain.repository
|
||||
|
||||
/**
|
||||
* 设置存储库
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.data.repository
|
||||
package com.taskttl.domain.repository
|
||||
|
||||
/**
|
||||
* 设置存储库
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.taskttl.data.repository
|
||||
package com.taskttl.domain.repository
|
||||
|
||||
import com.taskttl.data.local.model.Task
|
||||
import com.taskttl.domain.model.Task
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.taskttl.core.routes
|
||||
package com.taskttl.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.taskttl.presentation.onboarding.OnboardingScreen
|
||||
import com.taskttl.presentation.splash.SplashScreen
|
||||
import com.taskttl.presentation.features.onboarding.OnboardingScreen
|
||||
import com.taskttl.presentation.features.splash.SplashScreen
|
||||
|
||||
/**
|
||||
* 应用导航
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.taskttl.core.routes
|
||||
package com.taskttl.navigation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -14,28 +15,29 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import com.taskttl.core.routes.Routes.Main
|
||||
import com.taskttl.core.ui.CustomBottomBar
|
||||
import com.taskttl.presentation.countdown.CountdownDetailScreen
|
||||
import com.taskttl.presentation.countdown.CountdownEditScreen
|
||||
import com.taskttl.presentation.countdown.CountdownScreen
|
||||
import com.taskttl.presentation.settings.AboutScreen
|
||||
import com.taskttl.presentation.category.CategoryEditScreen
|
||||
import com.taskttl.presentation.category.CategoryScreen
|
||||
import com.taskttl.presentation.settings.DataManagementScreen
|
||||
import com.taskttl.presentation.settings.FeedbackScreen
|
||||
import com.taskttl.presentation.settings.PrivacyScreen
|
||||
import com.taskttl.presentation.settings.SettingsScreen
|
||||
import com.taskttl.presentation.statistics.StatisticsScreen
|
||||
import com.taskttl.presentation.task.TaskDetailScreen
|
||||
import com.taskttl.presentation.task.TaskEditorScreen
|
||||
import com.taskttl.presentation.task.TaskScreen
|
||||
import com.taskttl.presentation.common.components.CustomBottomBar
|
||||
import com.taskttl.presentation.features.auth.LoginScreen
|
||||
import com.taskttl.presentation.features.category.editor.CategoryEditScreen
|
||||
import com.taskttl.presentation.features.category.list.CategoryScreen
|
||||
import com.taskttl.presentation.features.countdown.detail.CountdownDetailScreen
|
||||
import com.taskttl.presentation.features.countdown.editor.CountdownEditScreen
|
||||
import com.taskttl.presentation.features.countdown.list.CountdownScreen
|
||||
import com.taskttl.presentation.features.settings.about.AboutScreen
|
||||
import com.taskttl.presentation.features.settings.dataManagement.DataManagementScreen
|
||||
import com.taskttl.presentation.features.settings.feedback.FeedbackScreen
|
||||
import com.taskttl.presentation.features.settings.main.SettingsScreen
|
||||
import com.taskttl.presentation.features.settings.privacy.PrivacyScreen
|
||||
import com.taskttl.presentation.features.statistics.StatisticsScreen
|
||||
import com.taskttl.presentation.features.task.detail.TaskEditorScreen
|
||||
import com.taskttl.presentation.features.task.editor.TaskDetailScreen
|
||||
import com.taskttl.presentation.features.task.list.TaskScreen
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.nav_countdown
|
||||
import taskttl.composeapp.generated.resources.nav_settings
|
||||
@@ -51,17 +53,18 @@ fun MainNav() {
|
||||
|
||||
val bottomItems = remember {
|
||||
listOf(
|
||||
Triple(Icons.AutoMirrored.Filled.List, Res.string.nav_todo, Main.Task),
|
||||
Triple(Icons.Default.AccessTime, Res.string.nav_countdown, Main.Countdown),
|
||||
Triple(Icons.Default.BarChart, Res.string.nav_statistics, Main.Statistics),
|
||||
Triple(Icons.Default.Settings, Res.string.nav_settings, Main.Settings),
|
||||
Triple(Icons.AutoMirrored.Filled.List, Res.string.nav_todo, Routes.Main.Task),
|
||||
Triple(Icons.Default.AccessTime, Res.string.nav_countdown, Routes.Main.Countdown),
|
||||
Triple(Icons.Default.BarChart, Res.string.nav_statistics, Routes.Main.Statistics),
|
||||
Triple(Icons.Default.Settings, Res.string.nav_settings, Routes.Main.Settings),
|
||||
)
|
||||
}
|
||||
|
||||
val currentDestination by mainNavController.currentBackStackEntryAsState()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.background(Color(0xffF9F9F9)),
|
||||
modifier = Modifier.fillMaxSize().background(Color(0xffF9F9F9)),
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
bottomBar = {
|
||||
CustomBottomBar(
|
||||
bottomItems = bottomItems,
|
||||
@@ -71,7 +74,7 @@ fun MainNav() {
|
||||
return@CustomBottomBar
|
||||
}
|
||||
mainNavController.navigate(route) {
|
||||
popUpTo(Main.Task) { saveState = true }
|
||||
popUpTo(Routes.Main.Task) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
@@ -80,116 +83,124 @@ fun MainNav() {
|
||||
}
|
||||
) { paddingValues ->
|
||||
NavHost(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.padding(bottom = paddingValues.calculateBottomPadding()),
|
||||
navController = mainNavController,
|
||||
startDestination = Main.Task
|
||||
startDestination = Routes.Main.Task
|
||||
) {
|
||||
// 任务
|
||||
composable<Main.Task> {
|
||||
composable<Routes.Main.Task> {
|
||||
TaskScreen(navController = mainNavController)
|
||||
}
|
||||
composable<Main.Task.AddTask> {
|
||||
composable<Routes.Main.Task.AddTask> {
|
||||
TaskEditorScreen(
|
||||
taskId = null,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable<Main.Task.EditTask> { backStackEntry ->
|
||||
val taskDetail: Main.Task.EditTask = backStackEntry.toRoute()
|
||||
composable<Routes.Main.Task.EditTask> { backStackEntry ->
|
||||
val taskDetail: Routes.Main.Task.EditTask = backStackEntry.toRoute()
|
||||
TaskEditorScreen(
|
||||
taskDetail.taskId,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable<Main.Task.TaskDetail> { backStackEntry ->
|
||||
val taskDetail: Main.Task.TaskDetail = backStackEntry.toRoute()
|
||||
composable<Routes.Main.Task.TaskDetail> { backStackEntry ->
|
||||
val taskDetail: Routes.Main.Task.TaskDetail = backStackEntry.toRoute()
|
||||
TaskDetailScreen(
|
||||
taskId = taskDetail.taskId,
|
||||
onNavigateBack = { mainNavController.popBackStack() },
|
||||
onNavigateToEdit = { mainNavController.navigate(Main.Task.EditTask(taskDetail.taskId)) }
|
||||
onNavigateToEdit = { mainNavController.navigate(Routes.Main.Task.EditTask(taskDetail.taskId)) }
|
||||
)
|
||||
}
|
||||
|
||||
// 倒数日
|
||||
composable<Main.Countdown> {
|
||||
composable<Routes.Main.Countdown> {
|
||||
CountdownScreen(navController = mainNavController)
|
||||
}
|
||||
composable<Main.Countdown.AddCountdown> {
|
||||
composable<Routes.Main.Countdown.AddCountdown> {
|
||||
CountdownEditScreen(
|
||||
countdownId = null,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable<Main.Countdown.EditCountdown> { backStackEntry ->
|
||||
val countdown: Main.Countdown.EditCountdown = backStackEntry.toRoute()
|
||||
composable<Routes.Main.Countdown.EditCountdown> { backStackEntry ->
|
||||
val countdown: Routes.Main.Countdown.EditCountdown = backStackEntry.toRoute()
|
||||
CountdownEditScreen(
|
||||
countdownId = countdown.countdownId,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable<Main.Countdown.CountdownDetail> { backStackEntry ->
|
||||
val countdown: Main.Countdown.CountdownDetail = backStackEntry.toRoute()
|
||||
composable<Routes.Main.Countdown.CountdownDetail> { backStackEntry ->
|
||||
val countdown: Routes.Main.Countdown.CountdownDetail = backStackEntry.toRoute()
|
||||
CountdownDetailScreen(
|
||||
countdownId = countdown.countdownId,
|
||||
onNavigateBack = { mainNavController.popBackStack() },
|
||||
onNavigateToEdit = {
|
||||
mainNavController.navigate(Main.Countdown.EditCountdown(countdown.countdownId))
|
||||
mainNavController.navigate(Routes.Main.Countdown.EditCountdown(countdown.countdownId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
composable<Main.Statistics> {
|
||||
composable<Routes.Main.Statistics> {
|
||||
StatisticsScreen(navController = mainNavController)
|
||||
}
|
||||
|
||||
// 设置界面
|
||||
composable<Main.Settings> {
|
||||
composable<Routes.Main.Settings> {
|
||||
SettingsScreen(navController = mainNavController)
|
||||
}
|
||||
// 分类管理
|
||||
composable<Main.Settings.CategoryManagement> { backStackEntry ->
|
||||
composable<Routes.Main.Settings.CategoryManagement> { backStackEntry ->
|
||||
CategoryScreen(
|
||||
navController = mainNavController,
|
||||
onAddCategory = { mainNavController.navigate(Main.Settings.AddCategory) },
|
||||
onAddCategory = { mainNavController.navigate(Routes.Main.Settings.AddCategory) },
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 添加分类
|
||||
composable<Main.Settings.AddCategory> {
|
||||
composable<Routes.Main.Settings.AddCategory> {
|
||||
CategoryEditScreen(
|
||||
categoryId = null,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 编辑分类
|
||||
composable<Main.Settings.EditCategory> { backStackEntry ->
|
||||
val editCategory: Main.Settings.EditCategory = backStackEntry.toRoute()
|
||||
composable<Routes.Main.Settings.EditCategory> { backStackEntry ->
|
||||
val editCategory: Routes.Main.Settings.EditCategory = backStackEntry.toRoute()
|
||||
CategoryEditScreen(
|
||||
categoryId = editCategory.categoryId,
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 数据管理
|
||||
composable<Main.Settings.DataManagement> {
|
||||
composable<Routes.Main.Settings.DataManagement> {
|
||||
DataManagementScreen(onNavigateBack = { mainNavController.popBackStack() })
|
||||
}
|
||||
// 反馈页面
|
||||
composable<Main.Settings.Feedback> {
|
||||
composable<Routes.Main.Settings.Feedback> {
|
||||
FeedbackScreen(onNavigateBack = { mainNavController.popBackStack() })
|
||||
}
|
||||
// 隐私
|
||||
composable<Main.Settings.Privacy> {
|
||||
composable<Routes.Main.Settings.Privacy> {
|
||||
PrivacyScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// 关于页面
|
||||
composable<Main.Settings.About> {
|
||||
composable<Routes.Main.Settings.About> {
|
||||
AboutScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
// 登录页面
|
||||
composable<Routes.Main.Settings.Login> {
|
||||
LoginScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.routes
|
||||
package com.taskttl.navigation
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -73,6 +73,9 @@ sealed interface Routes {
|
||||
|
||||
@Serializable
|
||||
data object About : Routes
|
||||
|
||||
@Serializable
|
||||
data object Login : Routes
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.ui.components
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -15,7 +16,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -53,6 +53,7 @@ fun AppHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(brush = gradient)
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 20.dp, vertical = 15.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -61,14 +62,14 @@ fun AppHeader(
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.back),
|
||||
tint = Color.White,
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.clickable { onBackClick() }
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
color = Color.White,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -77,7 +78,7 @@ fun AppHeader(
|
||||
Icon(
|
||||
imageVector = trailingIcon,
|
||||
contentDescription = stringResource(Res.string.action),
|
||||
tint = Color.White,
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.clickable { onTrailingClick?.invoke() }
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.ui.components
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -9,7 +9,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.domain.model.Category
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.all_text
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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
|
||||
@@ -26,8 +27,8 @@ import org.jetbrains.compose.resources.stringResource
|
||||
fun Chip(
|
||||
textRes: StringResource,
|
||||
icon: ImageVector? = null,
|
||||
backgroundColor: Color = Color(0xFFF5F5F5),
|
||||
contentColor: Color = Color(0xFF555555),
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -63,8 +64,8 @@ fun Chip(
|
||||
fun Chip(
|
||||
text: String,
|
||||
icon: ImageVector? = null,
|
||||
backgroundColor: Color = Color(0xFFF5F5F5),
|
||||
contentColor: Color = Color(0xFF555555),
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.ui.components
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -19,11 +20,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.navigation.Routes
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -42,14 +42,14 @@ fun CustomBottomBar(
|
||||
val barHeight = 56.dp
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(barHeight)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
modifier = Modifier.navigationBarsPadding().fillMaxWidth().height(barHeight)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(56.dp).background(Color.White)
|
||||
.height(56.dp).background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -84,7 +84,7 @@ fun BottomBarItem(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val color =
|
||||
if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Column(
|
||||
modifier = modifier
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.core.ui
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
@@ -71,9 +71,9 @@ fun LoadingOverlay(
|
||||
size = 56.dp,
|
||||
stroke = 6.dp,
|
||||
gradientColors = listOf(
|
||||
Color(0xFF4285F4),
|
||||
Color(0xFF73A7F9),
|
||||
Color(0xFF4285F4)
|
||||
MaterialTheme.colorScheme.primary,
|
||||
MaterialTheme.colorScheme.secondary,
|
||||
MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
@@ -0,0 +1,315 @@
|
||||
// package com.taskttl.presentation.common.components
|
||||
//
|
||||
// import androidx.annotation.DrawableRes
|
||||
// import androidx.compose.foundation.Canvas
|
||||
// import androidx.compose.foundation.Image
|
||||
// import androidx.compose.foundation.layout.aspectRatio
|
||||
// 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.wrapContentSize
|
||||
// import androidx.compose.foundation.shape.CircleShape
|
||||
// import androidx.compose.material3.FloatingActionButton
|
||||
// import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
// import androidx.compose.material3.FloatingActionButtonElevation
|
||||
// import androidx.compose.material3.Icon
|
||||
// import androidx.compose.material3.MaterialTheme
|
||||
// import androidx.compose.material3.NavigationBarItemDefaults
|
||||
// import androidx.compose.material3.Surface
|
||||
// import androidx.compose.material3.contentColorFor
|
||||
// import androidx.compose.runtime.Composable
|
||||
// import androidx.compose.ui.Modifier
|
||||
// import androidx.compose.ui.graphics.Color
|
||||
// import androidx.compose.ui.graphics.ColorFilter
|
||||
// import androidx.compose.ui.graphics.Path
|
||||
// import androidx.compose.ui.graphics.vector.ImageVector
|
||||
// import androidx.compose.ui.layout.SubcomposeLayout
|
||||
// import androidx.compose.ui.platform.LocalDensity
|
||||
// import androidx.compose.ui.res.painterResource
|
||||
// import androidx.compose.ui.unit.Dp
|
||||
// import androidx.compose.ui.unit.dp
|
||||
// import androidx.compose.ui.util.fastForEachIndexed
|
||||
// import androidx.compose.ui.util.fastMapIndexed
|
||||
// import org.jetbrains.compose.resources.painterResource
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * Note: fabeSize maintains the same height of modifier
|
||||
// */
|
||||
// @Composable
|
||||
// fun NotchedBottomBar(
|
||||
// modifier: Modifier = Modifier,
|
||||
// icons: List<ImageVector>,
|
||||
// selectedIndex: Int,
|
||||
// fabIcon: ImageVector,
|
||||
// fabIconSize: Dp = 28.dp,
|
||||
// onIconClick: (index: Int) -> Unit,
|
||||
// onFabClick: () -> Unit,
|
||||
// fabSize: Dp,
|
||||
// fabColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||
// fabContainerColor: Color = contentColorFor(fabColor),
|
||||
// fabElevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||
// iconColor: Color = NavigationBarItemDefaults.colors().unselectedIconColor,
|
||||
// selectedIconColor: Color = NavigationBarItemDefaults.colors().selectedIconColor,
|
||||
// iconContainerColor: Color = NavigationBarItemDefaults.colors().selectedIndicatorColor,
|
||||
// containerColor: Color = MaterialTheme.colorScheme.surfaceContainer
|
||||
// ) {
|
||||
// val density = LocalDensity.current
|
||||
// SubcomposeLayout(
|
||||
// modifier = modifier
|
||||
// ) { constraints ->
|
||||
// // measure fab
|
||||
// val fabPlaceables = subcompose("fab") {
|
||||
// FloatingActionButton(
|
||||
// onClick = onFabClick,
|
||||
// modifier = Modifier.size(fabSize),
|
||||
// containerColor = fabContainerColor,
|
||||
// contentColor = fabColor,
|
||||
// elevation = fabElevation,
|
||||
// shape = CircleShape
|
||||
// ) {
|
||||
// Icon(
|
||||
// modifier = Modifier.size(fabIconSize),
|
||||
// imageVector = fabIcon,
|
||||
// contentDescription = "fab",
|
||||
// tint = fabColor
|
||||
// )
|
||||
// }
|
||||
// }.map { it.measure(constraints) }
|
||||
//
|
||||
// val fabWidth = fabPlaceables.maxOf { it.width }
|
||||
// val fabHeight = fabPlaceables.maxOf { it.height }
|
||||
//
|
||||
// // measure icons
|
||||
// val iconPlaceables = icons.fastMapIndexed { index, icon ->
|
||||
// val measurable = subcompose("icon_$index") {
|
||||
// val selected = selectedIndex == index
|
||||
// Surface(
|
||||
// onClick = { onIconClick(index) },
|
||||
// shape = CircleShape,
|
||||
// modifier = Modifier
|
||||
// .padding(vertical = 12.dp)
|
||||
// .aspectRatio(16 / 9f),
|
||||
// color = if (selected) iconContainerColor else Color.Transparent,
|
||||
// contentColor = if (selected) selectedIconColor else iconColor
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = icon,
|
||||
// contentDescription = "$index-icon",
|
||||
// tint = if (selected) selectedIconColor else iconColor
|
||||
// )
|
||||
// }
|
||||
// }.first().measure(constraints)
|
||||
// measurable
|
||||
// }
|
||||
//
|
||||
// val canvasHeight = iconPlaceables.maxOf { it.height }
|
||||
//
|
||||
// // measure background
|
||||
// val backgroundPlaceables = subcompose("background") {
|
||||
// Canvas(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .height(with(density) { canvasHeight.toDp() })
|
||||
// ) {
|
||||
// val width = size.width
|
||||
// val height = size.height
|
||||
// val fabRadius = fabWidth / 2f
|
||||
// val centerX = width / 2f
|
||||
// val curveDepth = fabHeight * 0.6f
|
||||
// val controlOffsetX = fabWidth.toFloat()
|
||||
//
|
||||
// val path = Path().apply {
|
||||
// moveTo(0f, 0f)
|
||||
// lineTo(centerX - fabRadius - controlOffsetX, 0f)
|
||||
// cubicTo(
|
||||
// centerX - fabRadius, 0f,
|
||||
// centerX - fabRadius, curveDepth,
|
||||
// centerX, curveDepth
|
||||
// )
|
||||
// cubicTo(
|
||||
// centerX + fabRadius, curveDepth,
|
||||
// centerX + fabRadius, 0f,
|
||||
// centerX + fabRadius + controlOffsetX, 0f
|
||||
// )
|
||||
// lineTo(width, 0f)
|
||||
// lineTo(width, height)
|
||||
// lineTo(0f, height)
|
||||
// close()
|
||||
// }
|
||||
// drawPath(path, color = containerColor)
|
||||
// }
|
||||
// }.map { it.measure(constraints) }
|
||||
//
|
||||
// // calculate icon space
|
||||
// val totalItemsWidth = iconPlaceables.sumOf { it.width } + fabWidth
|
||||
// val spaceCount = icons.size + 1
|
||||
// val spaceWidth = (constraints.maxWidth - totalItemsWidth).coerceAtLeast(0) / spaceCount
|
||||
//
|
||||
// layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
// backgroundPlaceables.forEach {
|
||||
// it.place(0, constraints.maxHeight - it.height)
|
||||
// }
|
||||
//
|
||||
// var x = spaceWidth
|
||||
// iconPlaceables.fastForEachIndexed { index, it ->
|
||||
// it.place(x, 0)
|
||||
// x += it.width + if (index == (icons.size / 2 - 1)) {
|
||||
// spaceWidth + fabWidth // keep the space for fab
|
||||
// } else {
|
||||
// spaceWidth
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fabPlaceables.forEach {
|
||||
// it.place(
|
||||
// (constraints.maxWidth - it.width) / 2,
|
||||
// 0 - fabHeight / 2
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * Note: fabeSize maintains the same height of modifier
|
||||
// */
|
||||
// @Composable
|
||||
// fun NotchedBottomBar(
|
||||
// modifier: Modifier = Modifier,
|
||||
// icons: List<Int>,
|
||||
// selectedIndex: Int,
|
||||
// @DrawableRes fabIcon: Int,
|
||||
// fabIconSize: Dp = 28.dp,
|
||||
// onIconClick: (index: Int) -> Unit,
|
||||
// onFabClick: () -> Unit,
|
||||
// fabSize: Dp,
|
||||
// fabColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||
// fabContainerColor: Color = contentColorFor(fabColor),
|
||||
// fabElevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||
// iconColor: Color = NavigationBarItemDefaults.colors().unselectedIconColor,
|
||||
// selectedIconColor: Color = NavigationBarItemDefaults.colors().selectedIconColor,
|
||||
// iconContainerColor: Color = NavigationBarItemDefaults.colors().selectedIndicatorColor,
|
||||
// containerColor: Color = MaterialTheme.colorScheme.surfaceContainer
|
||||
// ) {
|
||||
// val density = LocalDensity.current
|
||||
// SubcomposeLayout(
|
||||
// modifier = modifier
|
||||
// ) { constraints ->
|
||||
// // measure fab
|
||||
// val fabPlaceables = subcompose("fab") {
|
||||
// FloatingActionButton(
|
||||
// onClick = onFabClick,
|
||||
// modifier = Modifier.size(fabSize),
|
||||
// containerColor = fabContainerColor,
|
||||
// contentColor = fabColor,
|
||||
// elevation = fabElevation,
|
||||
// shape = CircleShape
|
||||
// ) {
|
||||
// Image(
|
||||
// modifier = Modifier.size(fabIconSize),
|
||||
// painter = painterResource(fabIcon),
|
||||
// contentDescription = "fab",
|
||||
// colorFilter = ColorFilter.tint(fabColor)
|
||||
// )
|
||||
// }
|
||||
// }.map { it.measure(constraints) }
|
||||
//
|
||||
// val fabWidth = fabPlaceables.maxOf { it.width }
|
||||
// val fabHeight = fabPlaceables.maxOf { it.height }
|
||||
//
|
||||
// // measure icons
|
||||
// val iconPlaceables = icons.fastMapIndexed { index, icon ->
|
||||
// val measurable = subcompose("icon_$index") {
|
||||
// val selected = selectedIndex == index
|
||||
// Surface(
|
||||
// onClick = { onIconClick(index) },
|
||||
// shape = CircleShape,
|
||||
// modifier = Modifier
|
||||
// .padding(vertical = 12.dp)
|
||||
// .aspectRatio(16 / 9f),
|
||||
// color = if (selected) iconContainerColor else Color.Transparent,
|
||||
// contentColor = if (selected) selectedIconColor else iconColor
|
||||
// ) {
|
||||
// Image(
|
||||
// modifier= Modifier
|
||||
// .wrapContentSize().padding(vertical = 3.dp),
|
||||
// painter = painterResource(icon),
|
||||
// contentDescription = "$index-icon",
|
||||
// colorFilter = ColorFilter.tint(iconColor)
|
||||
// )
|
||||
// }
|
||||
// }.first().measure(constraints)
|
||||
// measurable
|
||||
// }
|
||||
//
|
||||
// val canvasHeight = iconPlaceables.maxOf { it.height }
|
||||
//
|
||||
// // measure background
|
||||
// val backgroundPlaceables = subcompose("background") {
|
||||
// Canvas(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .height(with(density) { canvasHeight.toDp() })
|
||||
// ) {
|
||||
// val width = size.width
|
||||
// val height = size.height
|
||||
// val fabRadius = fabWidth / 2f
|
||||
// val centerX = width / 2f
|
||||
// val curveDepth = fabHeight * 0.6f
|
||||
// val controlOffsetX = fabWidth.toFloat()
|
||||
//
|
||||
// val path = Path().apply {
|
||||
// moveTo(0f, 0f)
|
||||
// lineTo(centerX - fabRadius - controlOffsetX, 0f)
|
||||
// cubicTo(
|
||||
// centerX - fabRadius, 0f,
|
||||
// centerX - fabRadius, curveDepth,
|
||||
// centerX, curveDepth
|
||||
// )
|
||||
// cubicTo(
|
||||
// centerX + fabRadius, curveDepth,
|
||||
// centerX + fabRadius, 0f,
|
||||
// centerX + fabRadius + controlOffsetX, 0f
|
||||
// )
|
||||
// lineTo(width, 0f)
|
||||
// lineTo(width, height)
|
||||
// lineTo(0f, height)
|
||||
// close()
|
||||
// }
|
||||
// drawPath(path, color = containerColor)
|
||||
// }
|
||||
// }.map { it.measure(constraints) }
|
||||
//
|
||||
// // calculate icon space
|
||||
// val totalItemsWidth = iconPlaceables.sumOf { it.width } + fabWidth
|
||||
// val spaceCount = icons.size + 1
|
||||
// val spaceWidth = (constraints.maxWidth - totalItemsWidth).coerceAtLeast(0) / spaceCount
|
||||
//
|
||||
// layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
// backgroundPlaceables.forEach {
|
||||
// it.place(0, constraints.maxHeight - it.height)
|
||||
// }
|
||||
//
|
||||
// var x = spaceWidth
|
||||
// iconPlaceables.fastForEachIndexed { index, it ->
|
||||
// it.place(x, 0)
|
||||
// x += it.width + if (index == (icons.size / 2 - 1)) {
|
||||
// spaceWidth + fabWidth // keep the space for fab
|
||||
// } else {
|
||||
// spaceWidth
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fabPlaceables.forEach {
|
||||
// it.place(
|
||||
// (constraints.maxWidth - it.width) / 2,
|
||||
// 0 - fabHeight / 2
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.ui.components
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.taskttl.presentation.common.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.taskttl.core.manager.ThemeMode
|
||||
|
||||
/**
|
||||
* 主题模式选择弹窗
|
||||
*/
|
||||
@Composable
|
||||
fun ThemeModeDialog(
|
||||
currentMode: ThemeMode,
|
||||
onModeSelected: (ThemeMode) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "选择主题模式",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
// 明亮模式
|
||||
ThemeModeDialogOption(
|
||||
label = "☀️ 明亮",
|
||||
description = "始终使用明亮主题",
|
||||
selected = currentMode == ThemeMode.LIGHT,
|
||||
onClick = { onModeSelected(ThemeMode.LIGHT) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 暗黑模式
|
||||
ThemeModeDialogOption(
|
||||
label = "🌙 暗黑",
|
||||
description = "始终使用暗黑主题",
|
||||
selected = currentMode == ThemeMode.DARK,
|
||||
onClick = { onModeSelected(ThemeMode.DARK) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 跟随系统
|
||||
ThemeModeDialogOption(
|
||||
label = "🔄 跟随系统",
|
||||
description = "自动跟随系统主题设置",
|
||||
selected = currentMode == ThemeMode.SYSTEM,
|
||||
onClick = { onModeSelected(ThemeMode.SYSTEM) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 取消按钮
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗中的主题模式选项
|
||||
*/
|
||||
@Composable
|
||||
fun ThemeModeDialogOption(
|
||||
label: String,
|
||||
description: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
},
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.taskttl.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/** 浅色方案 - 优化后的配色方案(Material Design 3风格)*/
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
// 主色调 - 更鲜艳的紫蓝色系,提升视觉冲击力
|
||||
primary = Color(0xFF5B6FF8),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFFE1E4FF),
|
||||
onPrimaryContainer = Color(0xFF001A41),
|
||||
|
||||
// 次要色 - 优化紫色系,更柔和协调
|
||||
secondary = Color(0xFF8B5FBF),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFFF3E8FF),
|
||||
onSecondaryContainer = Color(0xFF2C0051),
|
||||
|
||||
// 第三色 - 温暖的橙色系(用于倒计时等)
|
||||
tertiary = Color(0xFFFF9500),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFFFFE6CC),
|
||||
onTertiaryContainer = Color(0xFF331F00),
|
||||
|
||||
// 错误色 - 更现代的红色系
|
||||
error = Color(0xFFDC3545),
|
||||
onError = Color(0xFFFFFFFF),
|
||||
errorContainer = Color(0xFFFFE5E5),
|
||||
onErrorContainer = Color(0xFF5F0010),
|
||||
|
||||
// 背景色 - 更柔和的浅色背景
|
||||
background = Color(0xFFFAFBFC),
|
||||
onBackground = Color(0xFF1A1C1E),
|
||||
|
||||
// 表面色 - 纯净白色
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onSurface = Color(0xFF1A1C1E),
|
||||
surfaceVariant = Color(0xFFE4E6EB),
|
||||
onSurfaceVariant = Color(0xFF45464F),
|
||||
|
||||
// 轮廓色 - 更细腻的边框
|
||||
outline = Color(0xFFCED0D6),
|
||||
outlineVariant = Color(0xFFE4E6EB),
|
||||
|
||||
// 其他
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFF2F3033),
|
||||
inverseOnSurface = Color(0xFFF1F0F4),
|
||||
inversePrimary = Color(0xFFAAB4FF),
|
||||
|
||||
// 表面容器色 - 更丰富的层次
|
||||
surfaceDim = Color(0xFFDBDCE0),
|
||||
surfaceBright = Color(0xFFFFFFFF),
|
||||
surfaceContainerLowest = Color(0xFFFFFFFF),
|
||||
surfaceContainerLow = Color(0xFFF5F6FA),
|
||||
surfaceContainer = Color(0xFFEFF0F5),
|
||||
surfaceContainerHigh = Color(0xFFE9EAF0),
|
||||
surfaceContainerHighest = Color(0xFFE3E4EA)
|
||||
)
|
||||
|
||||
/** 深色配色方案 - 优化后的深色模式(Material Design 3风格)*/
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
// 主色调 - 柔和亮丽的紫蓝色系
|
||||
primary = Color(0xFFAAB4FF),
|
||||
onPrimary = Color(0xFF001A41),
|
||||
primaryContainer = Color(0xFF3D4DB8),
|
||||
onPrimaryContainer = Color(0xFFE1E4FF),
|
||||
|
||||
// 次要色 - 优雅的亮紫色系
|
||||
secondary = Color(0xFFD4BCFF),
|
||||
onSecondary = Color(0xFF2C0051),
|
||||
secondaryContainer = Color(0xFF5A3D85),
|
||||
onSecondaryContainer = Color(0xFFF3E8FF),
|
||||
|
||||
// 第三色 - 温暖的亮橙色系
|
||||
tertiary = Color(0xFFFFBB5C),
|
||||
onTertiary = Color(0xFF331F00),
|
||||
tertiaryContainer = Color(0xFFCC7700),
|
||||
onTertiaryContainer = Color(0xFFFFE6CC),
|
||||
|
||||
// 错误色 - 柔和的亮红色系
|
||||
error = Color(0xFFFF7782),
|
||||
onError = Color(0xFF5F0010),
|
||||
errorContainer = Color(0xFFB42734),
|
||||
onErrorContainer = Color(0xFFFFE5E5),
|
||||
|
||||
// 背景色 - OLED友好的纯黑
|
||||
background = Color(0xFF1A1C1E),
|
||||
onBackground = Color(0xFFE3E2E6),
|
||||
|
||||
// 表面色 - 稍亮的深色
|
||||
surface = Color(0xFF1A1C1E),
|
||||
onSurface = Color(0xFFE3E2E6),
|
||||
surfaceVariant = Color(0xFF45464F),
|
||||
onSurfaceVariant = Color(0xFFC5C6D0),
|
||||
|
||||
// 轮廓色 - 更好的边框可见度
|
||||
outline = Color(0xFF8F9099),
|
||||
outlineVariant = Color(0xFF45464F),
|
||||
|
||||
// 其他
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFFE3E2E6),
|
||||
inverseOnSurface = Color(0xFF2F3033),
|
||||
inversePrimary = Color(0xFF5B6FF8),
|
||||
|
||||
// 表面容器色 - 深色模式下的精细层次
|
||||
surfaceDim = Color(0xFF121316),
|
||||
surfaceBright = Color(0xFF393B3F),
|
||||
surfaceContainerLowest = Color(0xFF0D0E11),
|
||||
surfaceContainerLow = Color(0xFF1A1C1E),
|
||||
surfaceContainer = Color(0xFF1E2023),
|
||||
surfaceContainerHigh = Color(0xFF282A2D),
|
||||
surfaceContainerHighest = Color(0xFF333538)
|
||||
)
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
* @param [darkTheme] 黑暗主题
|
||||
* @param [content] 内容
|
||||
*/
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = mainTypography(),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.taskttl.presentation.features.auth
|
||||
|
||||
import com.taskttl.core.base.BaseState
|
||||
|
||||
|
||||
/**
|
||||
* 身份验证状态
|
||||
* @author admin
|
||||
* @date 2025/10/26
|
||||
* @constructor 创建[AuthState]
|
||||
* @param [isLoading] 正在加载
|
||||
* @param [isProcessing] 正在处理
|
||||
* @param [error] 错误
|
||||
*/
|
||||
data class AuthState(
|
||||
override val isLoading: Boolean = false,
|
||||
override val isProcessing: Boolean = false,
|
||||
override val error: String? = null,
|
||||
) : BaseState()
|
||||
|
||||
|
||||
/**
|
||||
* 身份验证意图
|
||||
* @author admin
|
||||
* @date 2025/10/26
|
||||
* @constructor 创建[AuthIntent]
|
||||
*/
|
||||
sealed class AuthIntent {
|
||||
/**
|
||||
* 使用谷歌登录
|
||||
* @author admin
|
||||
* @date 2025/10/26
|
||||
*/
|
||||
object LoginWithGoogle: AuthIntent()
|
||||
|
||||
/**
|
||||
* 使用脸书登录
|
||||
* @author admin
|
||||
* @date 2025/10/26
|
||||
*/
|
||||
object LoginWithFacebook: AuthIntent()
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份验证效果
|
||||
* @author admin
|
||||
* @date 2025/10/26
|
||||
* @constructor 创建[AuthEffect]
|
||||
*/
|
||||
sealed class AuthEffect {}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.taskttl.presentation.features.auth
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.taskttl.core.base.BaseViewModel
|
||||
import com.taskttl.domain.repository.AuthRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 身份验证视图模型
|
||||
* @author admin
|
||||
* @date 2025/10/26
|
||||
* @constructor 创建[AuthViewModel]
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
AuthIntent.LoginWithFacebook -> {
|
||||
// TODO: 调用 Facebook 登录逻辑
|
||||
println("Facebook 登录触发")
|
||||
viewModelScope.launch {
|
||||
authRepository.loginWithFacebook()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
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.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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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 coil3.compose.AsyncImage
|
||||
import com.taskttl.presentation.common.components.AppHeader
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.InternalResourceApi
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.app_name
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, InternalResourceApi::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: AuthViewModel = koinViewModel(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
AppHeader(
|
||||
title = Res.string.app_name,
|
||||
showBack = true,
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
AsyncImage(
|
||||
model = Res.getUri("drawable/ic_launcher.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.graphicsLayer {
|
||||
alpha = logoAlpha.value
|
||||
scaleX = logoScale.value
|
||||
scaleY = logoScale.value
|
||||
},
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "TaskTTL 登录",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "欢迎回来,请选择登录方式:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
AnimatedLoginButton(
|
||||
icon = Icons.Default.Language,
|
||||
text = "使用 Google 登录",
|
||||
backgroundColor = Color(0xFFDB4437),
|
||||
alpha = googleButtonAlpha.value,
|
||||
onClick = { viewModel.handleIntent(AuthIntent.LoginWithGoogle) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AnimatedLoginButton(
|
||||
icon = Icons.Default.Facebook,
|
||||
text = "使用 Facebook 登录",
|
||||
backgroundColor = Color(0xFF1877F2),
|
||||
alpha = facebookButtonAlpha.value,
|
||||
onClick = { viewModel.handleIntent(AuthIntent.LoginWithFacebook) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "© DevTTL Team. All rights reserved.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedLoginButton(
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
fontWeight = FontWeight.Medium,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.presentation.category
|
||||
package com.taskttl.presentation.features.category.editor
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
@@ -45,16 +45,16 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.core.ui.LoadingOverlay
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryColor
|
||||
import com.taskttl.data.local.model.CategoryIcon
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.state.CategoryEffect
|
||||
import com.taskttl.data.state.CategoryIntent
|
||||
import com.taskttl.data.viewmodel.CategoryViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryColor
|
||||
import com.taskttl.domain.model.CategoryIcon
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import com.taskttl.presentation.common.components.AppHeader
|
||||
import com.taskttl.presentation.common.components.LoadingOverlay
|
||||
import com.taskttl.presentation.features.category.list.CategoryEffect
|
||||
import com.taskttl.presentation.features.category.list.CategoryIntent
|
||||
import com.taskttl.presentation.features.category.list.CategoryViewModel
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -337,11 +337,11 @@ private fun IconOption(item: CategoryIcon, selected: Boolean, onClick: () -> Uni
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.06f) else Color.White)
|
||||
.background(if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.border(
|
||||
BorderStroke(
|
||||
if (selected) 2.dp else 1.dp,
|
||||
if (selected) MaterialTheme.colorScheme.primary else Color(0xFFE6E6E6)
|
||||
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||
),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
@@ -351,7 +351,7 @@ private fun IconOption(item: CategoryIcon, selected: Boolean, onClick: () -> Uni
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = stringResource(item.displayNameRes),
|
||||
tint = Color(0xFF666666)
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -375,7 +375,7 @@ private fun ColorOption(
|
||||
.border(
|
||||
BorderStroke(
|
||||
if (selected) 2.dp else 0.dp,
|
||||
if (selected) Color.Black else Color.Transparent
|
||||
if (selected) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||
),
|
||||
CircleShape
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.presentation.category
|
||||
package com.taskttl.presentation.features.category.list
|
||||
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
@@ -53,20 +53,15 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.routes.Routes.Main
|
||||
import com.taskttl.core.ui.ActionButtonListItem
|
||||
import com.taskttl.core.ui.ErrorDialog
|
||||
import com.taskttl.core.ui.LoadingOverlay
|
||||
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.state.CategoryEffect
|
||||
import com.taskttl.data.state.CategoryIntent
|
||||
import com.taskttl.data.state.TaskEffect
|
||||
import com.taskttl.data.state.TaskIntent
|
||||
import com.taskttl.data.viewmodel.CategoryViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import com.taskttl.navigation.Routes
|
||||
import com.taskttl.presentation.common.components.ActionButtonListItem
|
||||
import com.taskttl.presentation.common.components.AppHeader
|
||||
import com.taskttl.presentation.common.components.ErrorDialog
|
||||
import com.taskttl.presentation.common.components.LoadingOverlay
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
@@ -84,7 +79,7 @@ fun CategoryScreen(
|
||||
navController: NavHostController,
|
||||
onAddCategory: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CategoryViewModel = koinViewModel()
|
||||
viewModel: CategoryViewModel = koinViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
@@ -94,6 +89,7 @@ fun CategoryScreen(
|
||||
is CategoryEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
|
||||
is CategoryEffect.NavigateBack -> {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
@@ -114,7 +110,7 @@ fun CategoryScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -186,7 +182,7 @@ fun CategoryScreen(
|
||||
isOpen = isOpen,
|
||||
onOpenChange = {},
|
||||
onEditClick = {
|
||||
navController.navigate(Main.Settings.EditCategory(category.id))
|
||||
navController.navigate(Routes.Main.Settings.EditCategory(category.id))
|
||||
},
|
||||
onDeleteClick = {
|
||||
viewModel.handleIntent(CategoryIntent.DeleteCategory(category.id))
|
||||
@@ -204,7 +200,7 @@ fun CategoryScreen(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(20.dp),
|
||||
containerColor = Color(0xFF667EEA)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
@@ -223,7 +219,7 @@ fun CategoryCardItem(
|
||||
onOpenChange: (Boolean) -> Unit,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ActionButtonListItem(
|
||||
modifier = Modifier
|
||||
@@ -236,7 +232,7 @@ fun CategoryCardItem(
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth().background(Color.Transparent),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Row(
|
||||
@@ -313,8 +309,8 @@ fun CategoryCardItem(
|
||||
.size(32.dp)
|
||||
.aspectRatio(1f),
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = Color(0xffff1111),
|
||||
contentColor = Color.White
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
@@ -329,7 +325,7 @@ fun CategoryCardItem(
|
||||
fun CategoryFilterTabs(
|
||||
selectedType: CategoryType,
|
||||
onSelected: (CategoryType) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val containerShape = RoundedCornerShape(8.dp)
|
||||
val tabShape = RoundedCornerShape(6.dp)
|
||||
@@ -337,19 +333,17 @@ fun CategoryFilterTabs(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = Color(0xFFF5F5F5), shape = containerShape)
|
||||
.border(width = 1.dp, color = Color(0xFFE0E0E0), shape = containerShape)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceContainer, shape = containerShape)
|
||||
.border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = containerShape)
|
||||
.padding(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CategoryType.entries.forEach { type ->
|
||||
val active = type == selectedType
|
||||
val textColor by animateColorAsState(
|
||||
if (active) Color(0xFF333333) else Color(
|
||||
0xFF666666
|
||||
)
|
||||
if (active) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
val backgroundColor by animateColorAsState(if (active) Color.White else Color.Transparent)
|
||||
val backgroundColor by animateColorAsState(if (active) MaterialTheme.colorScheme.surface else Color.Transparent)
|
||||
val elevation = if (active) 4.dp else 0.dp
|
||||
|
||||
Box(
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.taskttl.data.state
|
||||
package com.taskttl.presentation.features.category.list
|
||||
|
||||
import com.taskttl.core.viewmodel.BaseUiState
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryStatistics
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.core.base.BaseState
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryStatistics
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
|
||||
/**
|
||||
* 类别状态
|
||||
@@ -37,7 +37,7 @@ data class CategoryState(
|
||||
val showAddDialog: Boolean = false,
|
||||
val showEditDialog: Boolean = false,
|
||||
val showDeleteDialog: Boolean = false
|
||||
): BaseUiState()
|
||||
): BaseState()
|
||||
|
||||
/**
|
||||
* 类别意图
|
||||
@@ -1,13 +1,10 @@
|
||||
package com.taskttl.data.viewmodel
|
||||
package com.taskttl.presentation.features.category.list
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.taskttl.core.viewmodel.BaseViewModel
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.repository.CategoryRepository
|
||||
import com.taskttl.data.state.CategoryEffect
|
||||
import com.taskttl.data.state.CategoryIntent
|
||||
import com.taskttl.data.state.CategoryState
|
||||
import com.taskttl.core.base.BaseViewModel
|
||||
import com.taskttl.domain.model.Category
|
||||
import com.taskttl.domain.model.CategoryType
|
||||
import com.taskttl.domain.repository.CategoryRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
@@ -22,7 +22,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.domain.model.Category
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.taskttl.presentation.countdown
|
||||
package com.taskttl.presentation.features.countdown.detail
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -29,17 +29,18 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.ui.Chip
|
||||
import com.taskttl.core.ui.LoadingOverlay
|
||||
|
||||
import com.taskttl.core.utils.DateUtils
|
||||
import com.taskttl.data.state.CountdownEffect
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import com.taskttl.presentation.common.components.AppHeader
|
||||
import com.taskttl.presentation.common.components.Chip
|
||||
import com.taskttl.presentation.common.components.LoadingOverlay
|
||||
import com.taskttl.presentation.features.countdown.list.CountdownEffect
|
||||
import com.taskttl.presentation.features.countdown.list.CountdownViewModel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
@@ -90,7 +91,7 @@ fun CountdownDetailScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -117,7 +118,7 @@ fun CountdownDetailScreen(
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(
|
||||
brush = androidx.compose.ui.graphics.Brush.linearGradient(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
countdown.category.color.backgroundColor,
|
||||
Color.Transparent
|
||||
@@ -132,7 +133,7 @@ fun CountdownDetailScreen(
|
||||
text = countdown.title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color(0xFF111111)
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Chip(text = countdown.category.name)
|
||||
@@ -147,7 +148,7 @@ fun CountdownDetailScreen(
|
||||
Text(
|
||||
text = countdown.targetDate.toString(),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF444444)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -161,13 +162,13 @@ fun CountdownDetailScreen(
|
||||
text = daysRemaining.toString(),
|
||||
fontSize = 44.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color(0xFF111111)
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.label_days),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF666666)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -180,14 +181,14 @@ fun CountdownDetailScreen(
|
||||
text = stringResource(Res.string.event_description),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF333333)
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column {
|
||||
@@ -208,7 +209,7 @@ fun CountdownDetailScreen(
|
||||
text = stringResource(Res.string.detail_information),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF333333)
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Column {
|
||||
@@ -217,7 +218,7 @@ fun CountdownDetailScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
InfoItem(
|
||||
iconTint = Color(0xFF667EEA),
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
text = "${stringResource(Res.string.reminder)}:${
|
||||
stringResource(countdown.notificationEnabled.displayNameRes)
|
||||
}",
|
||||
@@ -227,7 +228,7 @@ fun CountdownDetailScreen(
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
InfoItem(
|
||||
iconTint = Color(0xFF999999),
|
||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
text = "${stringResource(Res.string.label_created_at)}${countdown.createdAt}",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
@@ -246,7 +247,7 @@ private fun InfoItem(iconTint: Color, text: String, modifier: Modifier = Modifie
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.White)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -257,6 +258,6 @@ private fun InfoItem(iconTint: Color, text: String, modifier: Modifier = Modifie
|
||||
.background(iconTint)
|
||||
) {}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = text, fontSize = 13.sp, color = Color(0xFF555555))
|
||||
Text(text = text, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user