Compare commits

...

3 Commits

Author SHA1 Message Date
Hsy
e954d9d3ed 重构 2025-10-29 14:47:59 +08:00
9082f15b63 通知更新 2025-10-20 22:57:32 +08:00
1ac177be8b 主视图优化 2025-10-20 22:57:32 +08:00
147 changed files with 2932 additions and 1667 deletions

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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() }
}

View File

@@ -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())
}
}

View File

@@ -1,4 +1,4 @@
package com.taskttl.core.ui
package com.taskttl.core.common
import android.annotation.SuppressLint
import android.graphics.Bitmap

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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())
}
}

View File

@@ -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 无法强制撤销权限,通常无法实现
}
}

View File

@@ -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}")
}
}
}

View File

@@ -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 -> ""
}
}
}

View File

@@ -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)
}
}

View File

@@ -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() }
}

View File

@@ -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")
}
}
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()
}
}

View 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()
}
}

View 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 {}

View File

@@ -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) }
}
}
}

View File

@@ -0,0 +1,5 @@
package com.taskttl.app.di
import org.koin.core.module.Module
expect val serviceModule: Module

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -1,4 +1,4 @@
package com.taskttl.core.ui
package com.taskttl.core.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -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数据库

View File

@@ -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)
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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] 有效载荷

View File

@@ -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()
}

View File

@@ -0,0 +1,25 @@
package com.taskttl.core.permission
/**
* 跨平台精确闹钟权限处理器
*/
expect object ExactAlarmPermissionManager {
/**
* 请求精确闹钟权限
* Android 12+ 需要用户允许
* iOS/其他平台无需操作,直接回调 true
*
*/
fun requestPermission(): Boolean
/**
* 验证当前是否有精确闹钟权限
*/
fun verifyPermission(): Boolean
/**
* 禁用或撤销权限(如果平台支持)
*/
fun disablePermission()
}

View File

@@ -0,0 +1,17 @@
package com.taskttl.core.permission
/**
* 跨平台通知权限处理器
*/
expect object NotificationPermissionManager {
/**
* 请求通知权限
* @return 是否允许
*/
fun requestPermission(): Boolean
fun verifyPermission(): Boolean
fun disablePermission()
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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

View File

@@ -22,4 +22,4 @@ expect object DeviceUtils {
*/
suspend fun getDeviceInfo(): BaseReq
}
}

View File

@@ -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"
}

View File

@@ -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)
}

View File

@@ -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
/**

View File

@@ -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
/**

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
/**

View File

@@ -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
/**

View File

@@ -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
/**

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}

View File

@@ -1,4 +1,4 @@
package com.taskttl.data.network.domain.resp
package com.taskttl.data.source.remote.dto.response
import kotlinx.serialization.Serializable

View File

@@ -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

View File

@@ -1,4 +1,4 @@
package com.taskttl.data.local.model
package com.taskttl.domain.model
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
/**

View File

@@ -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
/**

View File

@@ -1,4 +1,4 @@
package com.taskttl.data.repository
package com.taskttl.domain.repository
/**
* 设置存储库

View File

@@ -1,4 +1,4 @@
package com.taskttl.data.repository
package com.taskttl.domain.repository
/**
* 设置存储库

View File

@@ -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
/**

View File

@@ -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
/**
* 应用导航

View File

@@ -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() }
)
}
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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() }
)
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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
// )
// }
// }
// }
// }
//

View File

@@ -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

View File

@@ -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
)
)
}
}
}

View File

@@ -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
)
}

View File

@@ -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 {}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -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
)

View File

@@ -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(

View File

@@ -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()
/**
* 类别意图

View File

@@ -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

View File

@@ -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)

View File

@@ -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