更新
This commit is contained in:
parent
f06e79997a
commit
25b82ddd12
@ -1,14 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.todottl"
|
||||
namespace = "com.taskttl"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.todottl"
|
||||
applicationId = "com.taskttl"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
@ -28,6 +30,14 @@ android {
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
// 混淆开关
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_19
|
||||
@ -50,9 +60,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
@ -67,6 +74,27 @@ dependencies {
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
// core
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.splashscreen)
|
||||
|
||||
// navigation
|
||||
implementation(libs.androidx.navigation)
|
||||
|
||||
// hilt
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.android.navigation)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
// Room
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
// ViewModel
|
||||
implementation(libs.viewmodel.ktx)
|
||||
implementation(libs.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
implementation(libs.gson)
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".di.TaskApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@ -10,12 +11,12 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TodoTTL"
|
||||
android:theme="@style/Theme.TaskTTL"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.TodoTTL">
|
||||
android:theme="@style/Theme.TaskTTL">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
26
app/src/main/java/com/taskttl/MainActivity.kt
Normal file
26
app/src/main/java/com/taskttl/MainActivity.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package com.taskttl
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import com.taskttl.ui.TaskListApp
|
||||
import com.taskttl.ui.rememberAppState
|
||||
import com.taskttl.ui.theme.TaskTTLTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
val appState = rememberAppState()
|
||||
|
||||
TaskTTLTheme {
|
||||
TaskListApp(appState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.taskttl.data
|
||||
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation.NamedNavArgument
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
val LocalNavHostSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
|
||||
val LocalAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
|
||||
|
||||
fun NavGraphBuilder.composableWithCompositionLocal(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
|
||||
) {
|
||||
composable(route = route, arguments = arguments) {
|
||||
CompositionLocalProvider(
|
||||
LocalAnimatedVisibilityScope provides this@composable
|
||||
) {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
}
|
22
app/src/main/java/com/taskttl/data/constant/Constant.kt
Normal file
22
app/src/main/java/com/taskttl/data/constant/Constant.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package com.taskttl.data.constant
|
||||
|
||||
import com.taskttl.R
|
||||
|
||||
object Constant {
|
||||
|
||||
// 图片列表
|
||||
val imageList = listOf(
|
||||
R.mipmap.task,
|
||||
R.mipmap.goal,
|
||||
R.mipmap.event
|
||||
)
|
||||
|
||||
var APP_NAME = R.string.appName
|
||||
const val BASE_URL = "https://api.taskttl.com/"
|
||||
|
||||
private const val DEFAULT_TRUE = true
|
||||
const val DEFAULT_FALSE = false
|
||||
|
||||
/** 是否打印日志 */
|
||||
const val PRINT_LOG = DEFAULT_TRUE
|
||||
}
|
36
app/src/main/java/com/taskttl/data/contract/AddContract.kt
Normal file
36
app/src/main/java/com/taskttl/data/contract/AddContract.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package com.taskttl.data.contract
|
||||
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
|
||||
/**
|
||||
* 添加状态
|
||||
* @author Hsy
|
||||
* @date 2024/09/07
|
||||
* @constructor 创建[AddState]
|
||||
* @param [isLoading] 正在加载
|
||||
*/
|
||||
data class AddState(
|
||||
val isLoading: Boolean = false,
|
||||
val taskEntity: TaskEntity = TaskEntity(),
|
||||
) : UiState
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[IndexEvent]
|
||||
*/
|
||||
sealed interface AddEvent : UiEvent {
|
||||
data class AddTask(val item: TaskEntity) : AddEvent
|
||||
data class LoadTask(val id: Int) : AddEvent
|
||||
data class UpdateTitle(val title: String) : AddEvent
|
||||
data class UpdateContent(val content: String) : AddEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[IndexEffect]
|
||||
*/
|
||||
sealed interface AddEffect : UiEffect {
|
||||
data class AddSuccess(val message: String) : AddEffect
|
||||
}
|
24
app/src/main/java/com/taskttl/data/contract/Contract.kt
Normal file
24
app/src/main/java/com/taskttl/data/contract/Contract.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package com.taskttl.data.contract
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[UiState]
|
||||
*/
|
||||
interface UiState
|
||||
|
||||
/**
|
||||
* 用户界面事件
|
||||
* @author devttl
|
||||
* @date 2024/08/12
|
||||
* @constructor 创建[UiEvent]
|
||||
*/
|
||||
interface UiEvent
|
||||
|
||||
/**
|
||||
* 用户界面效果
|
||||
* @author devttl
|
||||
* @date 2024/08/12
|
||||
* @constructor 创建[UiEffect]
|
||||
*/
|
||||
interface UiEffect
|
38
app/src/main/java/com/taskttl/data/contract/IndexContract.kt
Normal file
38
app/src/main/java/com/taskttl/data/contract/IndexContract.kt
Normal file
@ -0,0 +1,38 @@
|
||||
package com.taskttl.data.contract
|
||||
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[IndexState]
|
||||
* @param [isLoading]
|
||||
*/
|
||||
data class IndexState(
|
||||
val isLoading: Boolean = false,
|
||||
val uncompletedList: List<TaskEntity> = listOf(),
|
||||
val completedList: List<TaskEntity> = listOf(),
|
||||
) : UiState
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[IndexEvent]
|
||||
*/
|
||||
sealed interface IndexEvent : UiEvent {
|
||||
data class LoadData(
|
||||
val uncompletedList: List<TaskEntity>,
|
||||
val completedList: List<TaskEntity>
|
||||
) : IndexEvent
|
||||
|
||||
data class Completed(val id: Int) : IndexEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[IndexEffect]
|
||||
*/
|
||||
sealed interface IndexEffect : UiEffect {
|
||||
data class ShowToast(val message: String) : IndexEffect
|
||||
}
|
14
app/src/main/java/com/taskttl/data/ext/Modifier.kt
Normal file
14
app/src/main/java/com/taskttl/data/ext/Modifier.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package com.taskttl.data.ext
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun Modifier.cancelRipperClick(onClick: () -> Unit) = this.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
)
|
22
app/src/main/java/com/taskttl/data/local/AppDatabase.kt
Normal file
22
app/src/main/java/com/taskttl/data/local/AppDatabase.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package com.taskttl.data.local
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.taskttl.data.local.dao.TaskDao
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
|
||||
/**
|
||||
* 应用程序数据库
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[AppDatabase]
|
||||
*/
|
||||
@Database(entities = [TaskEntity::class], version = 1, exportSchema = false)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
/**
|
||||
* 待办Dao
|
||||
* @return [TaskDao]
|
||||
*/
|
||||
abstract fun taskDao(): TaskDao
|
||||
}
|
54
app/src/main/java/com/taskttl/data/local/dao/TaskDao.kt
Normal file
54
app/src/main/java/com/taskttl/data/local/dao/TaskDao.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package com.taskttl.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
|
||||
@Dao
|
||||
interface TaskDao {
|
||||
|
||||
/**
|
||||
* 获取全部
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
@Query("SELECT * FROM tasks")
|
||||
suspend fun getAll(): List<TaskEntity>
|
||||
|
||||
/**
|
||||
* 获取待办事项由id
|
||||
* @param [id] id
|
||||
* @return [TaskEntity]
|
||||
*/
|
||||
@Query("SELECT * FROM tasks WHERE id = :id")
|
||||
suspend fun getTaskById(id: Int): TaskEntity
|
||||
|
||||
/**
|
||||
* 获取未完成待办事项
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
@Query("SELECT * FROM tasks WHERE completed = 0")
|
||||
suspend fun getUncompletedTask(): List<TaskEntity>
|
||||
|
||||
/**
|
||||
* 获取完成状态待办事项
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
@Query("SELECT * FROM tasks WHERE completed = 1")
|
||||
suspend fun getCompletedTask(): List<TaskEntity>
|
||||
|
||||
/**
|
||||
* 获取完成状态待办事项
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun addTask(task: TaskEntity): Long
|
||||
|
||||
/**
|
||||
* 完成待办事项
|
||||
* @param [id] id
|
||||
*/
|
||||
@Query("UPDATE tasks SET completed = 1 WHERE id = :id")
|
||||
suspend fun completedTask(id: Int)
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.taskttl.data.local.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
|
||||
@Entity(tableName = "tasks")
|
||||
data class TaskEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String = "",
|
||||
@ColumnInfo(name = "category")
|
||||
val category: Int = 0,
|
||||
@ColumnInfo(name = "content")
|
||||
val content: String = "",
|
||||
@ColumnInfo(name = "due_date")
|
||||
val dueDate: Long = 0,
|
||||
@ColumnInfo(name = "completed")
|
||||
val completed: Boolean = false,
|
||||
@ColumnInfo(name = "completed_date")
|
||||
val completedDate: Long = 0,
|
||||
)
|
@ -0,0 +1,54 @@
|
||||
package com.taskttl.data.repository
|
||||
|
||||
import com.taskttl.data.local.dao.TaskDao
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
class TaskRepository @Inject constructor(
|
||||
private val taskDao: TaskDao
|
||||
) {
|
||||
|
||||
/**
|
||||
* 从数据库获取任务
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
suspend fun getAll(): List<TaskEntity> {
|
||||
return taskDao.getAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待办事项由id
|
||||
* @param [id] id
|
||||
* @return [TaskEntity]
|
||||
*/
|
||||
suspend fun getTaskById(id: Int): TaskEntity {
|
||||
return taskDao.getTaskById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未完成待办事项
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
suspend fun getUncompletedTask(): List<TaskEntity> {
|
||||
return taskDao.getUncompletedTask()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完成状态待办事项
|
||||
* @return [List<TaskEntity>]
|
||||
*/
|
||||
suspend fun getCompletedTask(): List<TaskEntity> {
|
||||
return taskDao.getCompletedTask()
|
||||
}
|
||||
|
||||
suspend fun addTask(task: TaskEntity): Long {
|
||||
return taskDao.addTask(task)
|
||||
}
|
||||
|
||||
suspend fun completedTask(id: Int) {
|
||||
taskDao.completedTask(id)
|
||||
}
|
||||
}
|
30
app/src/main/java/com/taskttl/di/DataBaseModule.kt
Normal file
30
app/src/main/java/com/taskttl/di/DataBaseModule.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package com.taskttl.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.taskttl.data.local.AppDatabase
|
||||
import com.taskttl.data.local.dao.TaskDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DataBaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
name = "TaskTTL.db"
|
||||
).fallbackToDestructiveMigration().build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideTaskDao(db: AppDatabase): TaskDao = db.taskDao()
|
||||
}
|
26
app/src/main/java/com/taskttl/di/TaskApplication.kt
Normal file
26
app/src/main/java/com/taskttl/di/TaskApplication.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package com.taskttl.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.taskttl.utils.Logger
|
||||
import com.taskttl.utils.ToastUtil
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class TaskApplication : Application() {
|
||||
companion object {
|
||||
lateinit var appContext: Context
|
||||
private set
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// 初始化应用上下文
|
||||
appContext = applicationContext
|
||||
// 初始化Logger
|
||||
Logger.initialize(appContext)
|
||||
// 初始化吐司工具类
|
||||
ToastUtil.initialize(appContext)
|
||||
|
||||
}
|
||||
}
|
289
app/src/main/java/com/taskttl/ui/TaskListApp.kt
Normal file
289
app/src/main/java/com/taskttl/ui/TaskListApp.kt
Normal file
@ -0,0 +1,289 @@
|
||||
package com.taskttl.ui
|
||||
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.taskttl.R
|
||||
import com.taskttl.data.LocalAnimatedVisibilityScope
|
||||
import com.taskttl.data.LocalNavHostSharedTransitionScope
|
||||
import com.taskttl.data.composableWithCompositionLocal
|
||||
import com.taskttl.data.ext.cancelRipperClick
|
||||
import com.taskttl.ui.navigation.MainNavHost
|
||||
import com.taskttl.ui.navigation.MainTopNavItem
|
||||
import com.taskttl.ui.theme.TaskTTLTheme
|
||||
import com.taskttl.ui.viewmodel.SettingsViewModel
|
||||
|
||||
const val MAIN_ROUTE = "main"
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun TaskListApp(appState: TaskTTLAppState) {
|
||||
val settingsViewModel = hiltViewModel<SettingsViewModel>()
|
||||
|
||||
SharedTransitionLayout {
|
||||
CompositionLocalProvider(LocalNavHostSharedTransitionScope provides this) {
|
||||
AppNavHost(appState) {
|
||||
settingsViewModel.setShowSettingsDialog(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsViewModel.shouldShowSettingsDialog) {
|
||||
// SettingsDialog {
|
||||
// settingsViewModel.setShowSettingsDialog(false)
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppNavHost(appState: TaskTTLAppState, showSettingDialog: () -> Unit) {
|
||||
NavHost(
|
||||
navController = appState.navController,
|
||||
startDestination = MAIN_ROUTE
|
||||
) {
|
||||
composableWithCompositionLocal(MAIN_ROUTE) {
|
||||
MainRoute(
|
||||
mainAppState = rememberMainState(
|
||||
rememberNavController(), appState.snackBarHostState
|
||||
),
|
||||
settingClick = {
|
||||
showSettingDialog()
|
||||
},
|
||||
// navigateToContentDetail = {
|
||||
// appState.navigateToContentDetail(it)
|
||||
// },
|
||||
changeStatusBarIconMode = {
|
||||
appState.iconIsLight = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// homeDetailScreen {
|
||||
// appState.navController.popBackStack()
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainRoute(
|
||||
mainAppState: MainAppState,
|
||||
settingClick: () -> Unit,
|
||||
// navigateToContentDetail: (ContentBean) -> Unit,
|
||||
changeStatusBarIconMode: (Boolean) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0), // 这里这样写原因是"我的"页面是沉浸式
|
||||
containerColor = TaskTTLTheme.colors.background,
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = mainAppState.snackBarHostState)
|
||||
},
|
||||
topBar = {
|
||||
|
||||
},
|
||||
bottomBar = {
|
||||
TaskTTLBottomBar(
|
||||
mainAppState.mainTopLevelDestinations,
|
||||
mainAppState::navigateToMainTopLevelDestination,
|
||||
mainAppState.currentDestination,
|
||||
changeStatusBarIconMode
|
||||
) {
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) {
|
||||
MainNavHost(
|
||||
appState = mainAppState,
|
||||
onSettingClick = settingClick,
|
||||
// navigateToContentDetail = navigateToContentDetail
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun TaskTTLBottomBar(
|
||||
topLevelDestinations: List<MainTopNavItem>,
|
||||
navigateToTopLevelDestination: (MainTopNavItem, (Boolean) -> Unit) -> Unit,
|
||||
currentDestination: NavDestination?,
|
||||
changeStatusBarIconMode: (Boolean) -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val sharedTransitionScope = LocalNavHostSharedTransitionScope.current ?: return
|
||||
val animatedContentScope = LocalAnimatedVisibilityScope.current ?: return
|
||||
with(animatedContentScope) {
|
||||
with(sharedTransitionScope) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.renderInSharedTransitionScopeOverlay(
|
||||
zIndexInOverlay = 1f,
|
||||
)
|
||||
.animateEnterExit(
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
)
|
||||
.height(60.dp)
|
||||
.fillMaxWidth()
|
||||
.background(TaskTTLTheme.colors.background)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
topLevelDestinations.forEach {
|
||||
val selected = currentDestination?.route == it.route
|
||||
when (it) {
|
||||
MainTopNavItem.ADD -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color.Red, RoundedCornerShape(8.dp))
|
||||
.width(48.dp)
|
||||
.height(30.dp)
|
||||
.cancelRipperClick {
|
||||
navigateToTopLevelDestination(
|
||||
it,
|
||||
changeStatusBarIconMode
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
painter = painterResource(id = R.drawable.icon_add),
|
||||
contentDescription = stringResource(it.titleResId),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
TaskTTLBottomItemText(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
appNavItem = it,
|
||||
isSelected = selected,
|
||||
messageNum = if (MainTopNavItem.MY == it) 18 else null
|
||||
) {
|
||||
navigateToTopLevelDestination(it, changeStatusBarIconMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.notificationDot(num: Int) =
|
||||
composed {
|
||||
val color = TaskTTLTheme.colors.theme
|
||||
val textMeasure = rememberTextMeasurer()
|
||||
drawWithContent {
|
||||
drawContent()
|
||||
val centerOffset = Offset(size.width - 3.dp.toPx(), 3.dp.toPx())
|
||||
drawCircle(
|
||||
color = Color.Red,
|
||||
radius = 6.dp.toPx(),
|
||||
center = centerOffset
|
||||
)
|
||||
val textLayoutResult = textMeasure.measure(
|
||||
AnnotatedString(if (num < 100) "$num" else "99+"),
|
||||
TextStyle(fontSize = 7.sp, color = Color.White)
|
||||
)
|
||||
drawText(
|
||||
textLayoutResult,
|
||||
topLeft = Offset(
|
||||
centerOffset.x - textLayoutResult.size.width / 2f,
|
||||
centerOffset.y - textLayoutResult.size.height / 2f
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 底部导航栏文本
|
||||
*/
|
||||
@Composable
|
||||
private fun TaskTTLBottomItemText(
|
||||
modifier: Modifier = Modifier,
|
||||
appNavItem: MainTopNavItem,
|
||||
isSelected: Boolean,
|
||||
messageNum: Int? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.cancelRipperClick {
|
||||
onClick()
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
val fontSize by animateFloatAsState(
|
||||
targetValue = if (isSelected) 16.sp.value else 14.sp.value,
|
||||
animationSpec = tween(durationMillis = 100, easing = LinearEasing),
|
||||
label = "tabTextSize"
|
||||
)
|
||||
Text(
|
||||
modifier = if (messageNum != null) Modifier.notificationDot(messageNum) else Modifier,
|
||||
text = stringResource(appNavItem.titleResId),
|
||||
color = if (isSelected) TaskTTLTheme.colors.title else TaskTTLTheme.colors.body,
|
||||
lineHeight = 16.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
fontSize = fontSize.sp
|
||||
)
|
||||
}
|
||||
}
|
108
app/src/main/java/com/taskttl/ui/TaskTTLAppState.kt
Normal file
108
app/src/main/java/com/taskttl/ui/TaskTTLAppState.kt
Normal file
@ -0,0 +1,108 @@
|
||||
package com.taskttl.ui
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navOptions
|
||||
import com.google.gson.Gson
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import com.taskttl.ui.navigation.MainTopNavItem
|
||||
import com.taskttl.ui.screen.TASK_DETAIL_ROUTE
|
||||
import com.taskttl.ui.screen.navigateToAdd
|
||||
import com.taskttl.ui.screen.navigateToIndex
|
||||
import com.taskttl.ui.screen.navigateToMy
|
||||
|
||||
@Composable
|
||||
fun rememberAppState(
|
||||
navController: NavHostController = rememberNavController(),
|
||||
snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
): TaskTTLAppState {
|
||||
return remember(navController) {
|
||||
TaskTTLAppState(
|
||||
navController = navController,
|
||||
snackBarHostState = snackBarHostState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class TaskTTLAppState(
|
||||
val navController: NavHostController,
|
||||
val snackBarHostState: SnackbarHostState
|
||||
) {
|
||||
// 默认状态栏图标颜色是深色
|
||||
var iconIsLight by mutableStateOf(false)
|
||||
|
||||
val currentDestination: NavDestination?
|
||||
@Composable get() = navController.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
fun navigateToContentDetail(taskBean: TaskEntity) {
|
||||
navController.navigate(
|
||||
"$TASK_DETAIL_ROUTE/${Uri.encode(Gson().toJson(taskBean))}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun rememberMainState(
|
||||
navController: NavHostController = rememberNavController(),
|
||||
snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
): MainAppState {
|
||||
return remember(navController) {
|
||||
MainAppState(
|
||||
navController = navController,
|
||||
snackBarHostState = snackBarHostState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class MainAppState(
|
||||
val navController: NavHostController,
|
||||
val snackBarHostState: SnackbarHostState
|
||||
) {
|
||||
val mainTopLevelDestinations: List<MainTopNavItem> = MainTopNavItem.entries
|
||||
|
||||
val currentDestination: NavDestination?
|
||||
@Composable get() = navController.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
fun navigateToMainTopLevelDestination(
|
||||
topLevelDestination: MainTopNavItem, changeStatusBarIconMode: (Boolean) -> Unit
|
||||
) {
|
||||
val topLevelNavOptions = navOptions {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
when (topLevelDestination) {
|
||||
MainTopNavItem.INDEX -> {
|
||||
navController.navigateToIndex(topLevelNavOptions)
|
||||
changeStatusBarIconMode(false)
|
||||
}
|
||||
|
||||
MainTopNavItem.ADD -> {
|
||||
navController.navigateToAdd(topLevelNavOptions)
|
||||
changeStatusBarIconMode(true)
|
||||
}
|
||||
|
||||
MainTopNavItem.MY -> {
|
||||
navController.navigateToMy(topLevelNavOptions)
|
||||
changeStatusBarIconMode(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.taskttl.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@ExperimentalComposeUiApi
|
||||
@Composable
|
||||
fun BaseVerificationCodeTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
codeLength: Int = 6,
|
||||
onVerify: (String) -> Unit = {},
|
||||
codeBox: @Composable RowScope.(codeLength: Int, index: Int, code: String) -> Unit
|
||||
) {
|
||||
|
||||
//存储文本输入的值
|
||||
var text by remember { mutableStateOf("") }
|
||||
//管理当前获得焦点的文本框
|
||||
val focusManager = LocalFocusManager.current
|
||||
//用于请求焦点以显示软键盘
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
//它控制软键盘的显示和隐藏。
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = text,
|
||||
singleLine = true,
|
||||
onValueChange = { newText ->
|
||||
// 限制最大长度为6且只能输入数字
|
||||
if (newText.length <= codeLength && newText.all { it.isDigit() }) {
|
||||
text = newText
|
||||
|
||||
if (newText.length == codeLength) {
|
||||
onVerify(newText)
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
modifier = modifier
|
||||
.padding(horizontal = 26.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
keyboardController?.show()
|
||||
}
|
||||
}
|
||||
.wrapContentHeight(),
|
||||
readOnly = false,
|
||||
decorationBox = {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Transparent),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
for (i in 0 until codeLength) {
|
||||
codeBox(codeLength, i, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
22
app/src/main/java/com/taskttl/ui/components/FromLabel.kt
Normal file
22
app/src/main/java/com/taskttl/ui/components/FromLabel.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package com.taskttl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun FromLabel(title: String, top: Dp = 24.dp) {
|
||||
Text(
|
||||
title,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.padding(top = top, bottom = 8.dp)
|
||||
)
|
||||
}
|
39
app/src/main/java/com/taskttl/ui/components/NotTask.kt
Normal file
39
app/src/main/java/com/taskttl/ui/components/NotTask.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.taskttl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.R
|
||||
|
||||
@Composable
|
||||
fun NotTask() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
// 使用图标
|
||||
Icon(
|
||||
painter = painterResource(R.mipmap.assignment),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(120.dp),
|
||||
tint = Color.Gray
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(text = stringResource(R.string.not_task), color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
77
app/src/main/java/com/taskttl/ui/components/TaskItem.kt
Normal file
77
app/src/main/java/com/taskttl/ui/components/TaskItem.kt
Normal file
@ -0,0 +1,77 @@
|
||||
package com.taskttl.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.taskttl.data.constant.Constant
|
||||
import com.taskttl.data.contract.IndexEvent
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import com.taskttl.ui.viewmodel.IndexViewModel
|
||||
|
||||
@Composable
|
||||
fun TaskItem(
|
||||
taskEntity: TaskEntity,
|
||||
// navController: NavController,
|
||||
indexViewModel: IndexViewModel = hiltViewModel()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(80.dp)
|
||||
.padding(16.dp),
|
||||
// .clickable { navController.navigate("${RouteConfig.EDIT}/${taskEntity.id}") },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (taskEntity.completed) 0.5f else 1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(Constant.imageList[taskEntity.category]),
|
||||
modifier = Modifier.size(48.dp),
|
||||
contentDescription = ""
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
taskEntity.title,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
taskEntity.dueDate.toString(),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
Checkbox(
|
||||
checked = taskEntity.completed,
|
||||
modifier = Modifier.size(24.dp),
|
||||
onCheckedChange = {
|
||||
indexViewModel.sendEvent(IndexEvent.Completed(taskEntity.id))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package com.taskttl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private enum class CodeState {
|
||||
ENTERED,// 表示验证码已经完全输入
|
||||
INPUTTING,// 表示验证码正在输入中,还未完成。
|
||||
PENDING,// 表示验证码尚未开始输入。
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun VerificationCodeTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
onVerify: (String) -> Unit = {}
|
||||
) {
|
||||
|
||||
val baseSize = 276
|
||||
BaseVerificationCodeTextField(
|
||||
onVerify = onVerify
|
||||
) { codeLength, indxe, code ->
|
||||
// 判断当前位置是否有字符
|
||||
val isHasCode = indxe < code.length
|
||||
val fontSize = (144 / codeLength).sp
|
||||
|
||||
val codeState = when {
|
||||
isHasCode -> CodeState.ENTERED
|
||||
(indxe == code.length) -> CodeState.INPUTTING
|
||||
else -> CodeState.PENDING
|
||||
}
|
||||
val cardColor = when (codeState) {
|
||||
CodeState.ENTERED -> Color(0xFF466Eff)
|
||||
CodeState.INPUTTING -> Color.White
|
||||
CodeState.PENDING -> Color(0xFFF5F5F5)
|
||||
}
|
||||
val elevation = when (codeState) {
|
||||
CodeState.ENTERED -> 3.dp
|
||||
CodeState.INPUTTING -> 6.dp
|
||||
CodeState.PENDING -> 0.dp
|
||||
}
|
||||
val textColor = when (codeState) {
|
||||
CodeState.ENTERED -> Color.White
|
||||
CodeState.INPUTTING -> Color.Gray
|
||||
CodeState.PENDING -> Color.Gray
|
||||
}
|
||||
|
||||
val blinkInterval = 1000L
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(blinkInterval) {
|
||||
while (true) {
|
||||
isVisible = !isVisible
|
||||
delay(blinkInterval)
|
||||
}
|
||||
}
|
||||
key(elevation) {
|
||||
Card(
|
||||
Modifier
|
||||
.size((baseSize / codeLength).dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = cardColor
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = elevation,
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (codeState) {
|
||||
CodeState.ENTERED -> {
|
||||
Text(
|
||||
code[indxe].toString(), style = TextStyle(
|
||||
fontSize = fontSize,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
CodeState.INPUTTING -> {
|
||||
if (isVisible) {
|
||||
Text(
|
||||
"_", style = TextStyle(
|
||||
fontSize = fontSize,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CodeState.PENDING -> {
|
||||
// Text(
|
||||
// "_", style = TextStyle(
|
||||
// fontSize = fontSize,
|
||||
// color = textColor,
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
app/src/main/java/com/taskttl/ui/navigation/MainNavHost.kt
Normal file
39
app/src/main/java/com/taskttl/ui/navigation/MainNavHost.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.taskttl.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.taskttl.ui.MainAppState
|
||||
import com.taskttl.ui.screen.INDEX_ROUTE
|
||||
import com.taskttl.ui.screen.addScreen
|
||||
import com.taskttl.ui.screen.indexScreen
|
||||
import com.taskttl.ui.screen.myScreen
|
||||
|
||||
@Composable
|
||||
fun MainNavHost(
|
||||
appState: MainAppState,
|
||||
startDestination: String = INDEX_ROUTE,
|
||||
onSettingClick: () -> Unit,
|
||||
) {
|
||||
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = appState.navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
indexScreen(
|
||||
appState.snackBarHostState,
|
||||
// onSettingClick,
|
||||
// navigateToDetail = navigateToContentDetail
|
||||
)
|
||||
addScreen(snackBarHostState = appState.snackBarHostState)
|
||||
myScreen(snackBarHostState = appState.snackBarHostState)
|
||||
// composable(INDEX_ROUTE) { IndexScreen(navController) }
|
||||
// composable(
|
||||
// "${RouteConfig.EDIT}/{id}",
|
||||
// arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||
// ) { backStackEntry ->
|
||||
// backStackEntry.arguments?.getInt("id")?.let { AddScreen(navController, it) }
|
||||
// }
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.taskttl.ui.navigation
|
||||
|
||||
import com.taskttl.R
|
||||
import com.taskttl.ui.screen.ADD_ROUTE
|
||||
import com.taskttl.ui.screen.INDEX_ROUTE
|
||||
import com.taskttl.ui.screen.MY_ROUTE
|
||||
|
||||
enum class MainTopNavItem(val titleResId: Int, val route: String) {
|
||||
INDEX(R.string.nav_index, INDEX_ROUTE),
|
||||
ADD(R.string.nav_add, ADD_ROUTE),
|
||||
MY(R.string.nav_my, MY_ROUTE);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.todottl.ui.screen
|
||||
package com.taskttl.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@ -14,14 +14,19 @@ 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.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -30,19 +35,71 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.todottl.R
|
||||
import com.todottl.data.Constant
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import com.taskttl.R
|
||||
import com.taskttl.data.constant.Constant
|
||||
import com.taskttl.data.contract.AddEffect
|
||||
import com.taskttl.data.contract.AddEvent
|
||||
import com.taskttl.ui.components.FromLabel
|
||||
import com.taskttl.ui.viewmodel.AddViewModel
|
||||
import com.taskttl.ui.viewmodel.CollectSideEffect
|
||||
import com.taskttl.utils.DateUtil
|
||||
import com.taskttl.utils.ToastUtil
|
||||
|
||||
|
||||
/** 首页 */
|
||||
const val ADD_ROUTE = "add"
|
||||
const val TASK_DETAIL_ROUTE = "task_detail"
|
||||
|
||||
fun NavController.navigateToAdd(navOptions: NavOptions) = navigate(ADD_ROUTE, navOptions)
|
||||
|
||||
|
||||
fun NavGraphBuilder.addScreen(snackBarHostState: SnackbarHostState) {
|
||||
composable(route = ADD_ROUTE) {
|
||||
AddRoute(snackBarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddScreen(navController: NavController) {
|
||||
fun AddRoute(
|
||||
snackBarHostState: SnackbarHostState,
|
||||
id: Int = 0,
|
||||
addViewModel: AddViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by addViewModel.uiState.collectAsState()
|
||||
|
||||
// 使用 remember 保存状态
|
||||
var titleValid by remember { mutableStateOf(false) }
|
||||
|
||||
var date by remember { mutableStateOf("") }
|
||||
var contentValid by remember { mutableStateOf(false) }
|
||||
var categoryData by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(id) {
|
||||
if (id > 0) {
|
||||
addViewModel.sendEvent(AddEvent.LoadTask(id))
|
||||
}
|
||||
}
|
||||
|
||||
addViewModel.CollectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
is AddEffect.AddSuccess -> {
|
||||
ToastUtil.showLongToast(sideEffect.message)
|
||||
// navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@ -56,28 +113,16 @@ fun AddScreen(navController: NavController) {
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = ""
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.close),
|
||||
modifier = Modifier.size(48.dp),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"Add New Task",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp)
|
||||
.weight(1f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.add_new_task),
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 36.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@ -106,14 +151,22 @@ fun AddScreen(navController: NavController) {
|
||||
"Task Title", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
},
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
value = state.taskEntity.title,
|
||||
onValueChange = {
|
||||
addViewModel.sendEvent(AddEvent.UpdateTitle(it))
|
||||
titleValid = it.isNotEmpty()
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = colors
|
||||
colors = colors,
|
||||
isError = !titleValid
|
||||
)
|
||||
if (!titleValid) {
|
||||
Text(text = "Please enter valid text", color = Color.Red)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
@ -123,8 +176,7 @@ fun AddScreen(navController: NavController) {
|
||||
FromLabel("Category", 0.dp)
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Constant.imageList.forEachIndexed { index, item ->
|
||||
var modifier = Modifier
|
||||
.size(48.dp)
|
||||
var modifier = Modifier.size(48.dp)
|
||||
|
||||
if (categoryData == index) {
|
||||
modifier = Modifier
|
||||
@ -149,54 +201,28 @@ fun AddScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
FromLabel("Date")
|
||||
TextField(placeholder = {
|
||||
Text(
|
||||
"Date", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
},
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = colors,
|
||||
trailingIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.calendar),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
FromLabel("Time")
|
||||
TextField(placeholder = {
|
||||
Text(
|
||||
"Time", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
},
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = colors,
|
||||
trailingIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.clock),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
FromLabel("Date")
|
||||
TextField(placeholder = {
|
||||
Text(
|
||||
"Date", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
},
|
||||
value = date,
|
||||
onValueChange = {
|
||||
date = it
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = colors,
|
||||
trailingIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.calendar),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
FromLabel("Notes")
|
||||
TextField(
|
||||
placeholder = {
|
||||
@ -204,20 +230,32 @@ fun AddScreen(navController: NavController) {
|
||||
"Notes", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
},
|
||||
value = "",
|
||||
value = state.taskEntity.content,
|
||||
maxLines = 5,
|
||||
onValueChange = {},
|
||||
onValueChange = {
|
||||
addViewModel.sendEvent(AddEvent.UpdateContent(it))
|
||||
contentValid = it.isNotEmpty()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(177.dp),
|
||||
.height(140.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = colors
|
||||
colors = colors,
|
||||
isError = !contentValid
|
||||
)
|
||||
if (!contentValid) {
|
||||
Text(text = "Please enter valid text", color = Color.Red)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
// navController.navigate(RouteConfig.ADD)
|
||||
if (!titleValid || !contentValid) {
|
||||
ToastUtil.showLongToast("Please enter valid text")
|
||||
return@Button
|
||||
}
|
||||
DateUtil.getTodayStartLong()
|
||||
addViewModel.sendEvent(AddEvent.AddTask(state.taskEntity))
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
@ -226,18 +264,7 @@ fun AddScreen(navController: NavController) {
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(50.dp)
|
||||
) {
|
||||
Text(text = "Save")
|
||||
Text(text = stringResource(R.string.save))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FromLabel(title: String, top: Dp = 24.dp) {
|
||||
Text(
|
||||
title,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.padding(top = top, bottom = 8.dp)
|
||||
)
|
||||
}
|
249
app/src/main/java/com/taskttl/ui/screen/IndexScreen.kt
Normal file
249
app/src/main/java/com/taskttl/ui/screen/IndexScreen.kt
Normal file
@ -0,0 +1,249 @@
|
||||
package com.taskttl.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.gson.Gson
|
||||
import com.taskttl.R
|
||||
import com.taskttl.data.composableWithCompositionLocal
|
||||
import com.taskttl.data.ext.cancelRipperClick
|
||||
import com.taskttl.data.local.entity.TaskEntity
|
||||
import com.taskttl.ui.components.NotTask
|
||||
import com.taskttl.ui.components.TaskItem
|
||||
import com.taskttl.ui.viewmodel.IndexViewModel
|
||||
import com.taskttl.utils.DateUtil
|
||||
import com.taskttl.utils.Logger
|
||||
|
||||
/** 首页 */
|
||||
const val INDEX_ROUTE = "index"
|
||||
const val HOME_DETAIL_ROUTE = "home_detail"
|
||||
|
||||
fun NavController.navigateToIndex(navOptions: NavOptions) = navigate(INDEX_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.indexScreen(
|
||||
snackBarHostState: SnackbarHostState,
|
||||
// onSettingClick: () -> Unit,
|
||||
// navigateToDetail: (ContentBean) -> Unit
|
||||
) {
|
||||
composable(INDEX_ROUTE) {
|
||||
IndexScreen(
|
||||
snackBarHostState,
|
||||
// onSettingClick,
|
||||
// navigateToDetail
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.homeDetailScreen(
|
||||
navigateToHome: () -> Unit
|
||||
) {
|
||||
composableWithCompositionLocal(
|
||||
route = "$HOME_DETAIL_ROUTE/{content}",
|
||||
arguments = listOf(navArgument("content") { NavType.StringType })
|
||||
) {
|
||||
val contentBean = try {
|
||||
Gson().fromJson(it.arguments?.getString("content") ?: "", TaskEntity::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
HomeDetailRoute()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeDetailRoute() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Message")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页
|
||||
* @param [navController] NAV 控制器
|
||||
* @param [indexViewModel] 索引视图模型
|
||||
*/
|
||||
@Composable
|
||||
fun IndexScreen(snackBarHostState: SnackbarHostState) {
|
||||
val indexViewModel = hiltViewModel<IndexViewModel>();
|
||||
val state by indexViewModel.uiState.collectAsState()
|
||||
|
||||
var isButtonVisible by remember { mutableStateOf(true) }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
var backgroundHeight by remember { mutableStateOf(160.dp) }
|
||||
|
||||
val textTopPadding = remember(backgroundHeight) {
|
||||
mutableStateOf(calculateTextTopPadding(backgroundHeight))
|
||||
}
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.y
|
||||
var dp = backgroundHeight + delta.dp
|
||||
if (dp >= 160.dp) {
|
||||
dp = 160.dp
|
||||
}
|
||||
backgroundHeight = maxOf(96.dp, dp) // 最小高度 100.dp
|
||||
textTopPadding.value = calculateTextTopPadding(backgroundHeight)
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据滚动状态来更新按钮的可见性
|
||||
LaunchedEffect(remember { derivedStateOf { listState.firstVisibleItemIndex } }) {
|
||||
isButtonVisible = listState.firstVisibleItemIndex == 0
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF1F5F9))
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(backgroundHeight),
|
||||
shape = RoundedCornerShape(0.dp)
|
||||
) {
|
||||
Box() {
|
||||
// 背景图
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.header),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(backgroundHeight),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = "Header"
|
||||
)
|
||||
if (textTopPadding.value > 50.dp) {
|
||||
Text(
|
||||
DateUtil.today(),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 36.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.index_title),
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = textTopPadding.value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.uncompletedList.isEmpty() && state.completedList.isEmpty()) {
|
||||
NotTask()
|
||||
} else {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
// .align(Alignment.TopCenter)
|
||||
.padding(all = 10.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
) {
|
||||
items(state.uncompletedList) {
|
||||
TaskItem(it)
|
||||
if (state.uncompletedList.indexOf(it) != state.uncompletedList.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (isButtonVisible) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
// .align(Alignment.BottomCenter)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.cancelRipperClick {
|
||||
// navigateToDetail(contentBean)
|
||||
// navController.navigate(RouteConfig.ADD)
|
||||
},
|
||||
// shape = RoundedCornerShape(50.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.add_new_task))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算 Text 组件的顶部偏移
|
||||
fun calculateTextTopPadding(currentBackgroundHeight: Dp): Dp {
|
||||
// 这里可以根据 backgroundHeight 的不同值返回不同的顶部偏移量
|
||||
// 例如,保持文本始终位于背景图像的中心或某个固定位置。
|
||||
return when (currentBackgroundHeight.value) {
|
||||
in 96.0..160.0 -> (96 + (currentBackgroundHeight.value - 160)).dp
|
||||
else -> 96.dp // 默认偏移
|
||||
}
|
||||
}
|
67
app/src/main/java/com/taskttl/ui/screen/MyScreen.kt
Normal file
67
app/src/main/java/com/taskttl/ui/screen/MyScreen.kt
Normal file
@ -0,0 +1,67 @@
|
||||
package com.taskttl.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import com.taskttl.R
|
||||
|
||||
const val MY_ROUTE = "my"
|
||||
|
||||
fun NavController.navigateToMy(navOptions: NavOptions) = navigate(MY_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.myScreen(
|
||||
snackBarHostState: SnackbarHostState,
|
||||
// navigateToContentDetail: (ContentBean) -> Unit
|
||||
) {
|
||||
composable(route = MY_ROUTE) {
|
||||
MyRoute(snackBarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MyRoute(snackBarHostState: SnackbarHostState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF1F5F9))
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.add_header),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(96.dp),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = ""
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.nav_my),
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 36.dp)
|
||||
)
|
||||
}
|
||||
}
|
50
app/src/main/java/com/taskttl/ui/theme/Color.kt
Normal file
50
app/src/main/java/com/taskttl/ui/theme/Color.kt
Normal file
@ -0,0 +1,50 @@
|
||||
package com.taskttl.ui.theme
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
val RedBookRed = Color(0xFFFF2E4D)
|
||||
val WhiteWindow = Color(0xFFF5F6F7)
|
||||
val WhiteBackground = Color(0xFFFFFFFF)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val BlackWindow = Color(0xFF111111)
|
||||
val BlackBackground = Color(0xFF1F1D1D)
|
||||
|
||||
|
||||
data class LorenColors(
|
||||
val theme:Color,
|
||||
val window: Color,
|
||||
val background: Color,
|
||||
val title: Color,
|
||||
val body: Color,
|
||||
val icon: Color,
|
||||
val divider: Color
|
||||
)
|
||||
|
||||
val lightLorenColors = LorenColors(
|
||||
theme = RedBookRed,
|
||||
window = WhiteWindow,
|
||||
background = WhiteBackground,
|
||||
title = Color.Black,
|
||||
body = Color(0xFF666666),
|
||||
icon = Color.Black,
|
||||
divider = Color.LightGray
|
||||
)
|
||||
|
||||
val darkLorenColors = LorenColors(
|
||||
theme = RedBookRed,
|
||||
window = BlackWindow,
|
||||
background = BlackBackground,
|
||||
title = Color.White,
|
||||
body = Color(0xFF666666),
|
||||
icon = Color.White,
|
||||
divider = Color.DarkGray
|
||||
)
|
||||
|
||||
val LocalCustomColors = staticCompositionLocalOf { lightLorenColors }
|
@ -1,6 +1,5 @@
|
||||
package com.todottl.ui.theme
|
||||
package com.taskttl.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -34,7 +33,7 @@ private val LightColorScheme = lightColorScheme(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TodoTTLTheme(
|
||||
fun TaskTTLTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
@ -56,3 +55,12 @@ fun TodoTTLTheme(
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
object TaskTTLTheme {
|
||||
val colors: LorenColors
|
||||
@Composable
|
||||
get() = LocalCustomColors.current
|
||||
val textStyle: LorenTextStyle
|
||||
@Composable
|
||||
get() = LocalTextStyles.current
|
||||
}
|
88
app/src/main/java/com/taskttl/ui/theme/Type.kt
Normal file
88
app/src/main/java/com/taskttl/ui/theme/Type.kt
Normal file
@ -0,0 +1,88 @@
|
||||
package com.taskttl.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
||||
|
||||
@Immutable
|
||||
data class LorenTextStyle(
|
||||
val titleLarge: TextStyle,
|
||||
val titleMedium: TextStyle,
|
||||
val titleSmall: TextStyle,
|
||||
val bodyLarge: TextStyle,
|
||||
val bodyMedium: TextStyle,
|
||||
val bodySmall: TextStyle,
|
||||
)
|
||||
|
||||
val defaultTextStyle = LorenTextStyle(
|
||||
titleLarge = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp,
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
)
|
||||
)
|
||||
|
||||
val LocalTextStyles = staticCompositionLocalOf { defaultTextStyle }
|
46
app/src/main/java/com/taskttl/ui/viewmodel/AddViewModel.kt
Normal file
46
app/src/main/java/com/taskttl/ui/viewmodel/AddViewModel.kt
Normal file
@ -0,0 +1,46 @@
|
||||
package com.taskttl.ui.viewmodel
|
||||
|
||||
import com.taskttl.data.contract.AddEffect
|
||||
import com.taskttl.data.contract.AddEvent
|
||||
import com.taskttl.data.contract.AddState
|
||||
import com.taskttl.data.repository.TaskRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[AddViewModel]
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AddViewModel @Inject constructor(
|
||||
private val taskRepository: TaskRepository
|
||||
) : BaseViewModel<AddState, AddEvent, AddEffect>() {
|
||||
override fun initialState(): AddState = AddState();
|
||||
|
||||
override suspend fun handleEvent(event: AddEvent, state: AddState): AddState {
|
||||
return when (event) {
|
||||
is AddEvent.AddTask -> {
|
||||
val id = taskRepository.addTask(event.item)
|
||||
if (id > 0) {
|
||||
sendEffect(AddEffect.AddSuccess("任务添加成功"))
|
||||
}
|
||||
state.copy(isLoading = false)
|
||||
}
|
||||
|
||||
is AddEvent.LoadTask -> {
|
||||
val task = taskRepository.getTaskById(event.id)
|
||||
state.copy(taskEntity = task)
|
||||
}
|
||||
|
||||
is AddEvent.UpdateTitle -> {
|
||||
state.copy(taskEntity = state.taskEntity.copy(title = event.title))
|
||||
}
|
||||
|
||||
is AddEvent.UpdateContent -> {
|
||||
state.copy(taskEntity = state.taskEntity.copy(content = event.content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
132
app/src/main/java/com/taskttl/ui/viewmodel/BaseViewModel.kt
Normal file
132
app/src/main/java/com/taskttl/ui/viewmodel/BaseViewModel.kt
Normal file
@ -0,0 +1,132 @@
|
||||
package com.taskttl.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.taskttl.data.contract.UiEffect
|
||||
import com.taskttl.data.contract.UiEvent
|
||||
import com.taskttl.data.contract.UiState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 基础视图模型
|
||||
* @author devttl
|
||||
* @date 2024/08/12
|
||||
* @constructor 创建[BaseViewModel]
|
||||
*/
|
||||
abstract class BaseViewModel<S : UiState, E : UiEvent, F : UiEffect> : ViewModel() {
|
||||
|
||||
/** 初始状态 */
|
||||
private val initialState: S by lazy { initialState() }
|
||||
|
||||
/**
|
||||
* 初始状态
|
||||
* @return [S]
|
||||
*/
|
||||
protected abstract fun initialState(): S
|
||||
|
||||
/** 用户界面状态 */
|
||||
private val _uiState: MutableStateFlow<S> by lazy { MutableStateFlow(initialState) }
|
||||
|
||||
/** 用户界面状态 */
|
||||
val uiState: StateFlow<S> by lazy { _uiState }
|
||||
|
||||
/** 用户界面事件 */
|
||||
private val _uiEvent: MutableSharedFlow<E> = MutableSharedFlow()
|
||||
|
||||
/** 用户界面效果 */
|
||||
private val _uiEffect: MutableSharedFlow<F> = MutableSharedFlow()
|
||||
|
||||
/** 用户界面效果 */
|
||||
val uiEffect: Flow<F> = _uiEffect
|
||||
|
||||
/**
|
||||
* 处理事件
|
||||
* @param [event] 事件
|
||||
* @param [state] 状态
|
||||
* @return [S?]
|
||||
*/
|
||||
protected abstract suspend fun handleEvent(event: E, state: S): S?
|
||||
|
||||
init {
|
||||
subscribeEvents()
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集事件
|
||||
*/
|
||||
private fun subscribeEvents() {
|
||||
viewModelScope.launch {
|
||||
_uiEvent.collect {
|
||||
reduceEvent(_uiState.value, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送事件
|
||||
* @param [event] 事件
|
||||
*/
|
||||
fun sendEvent(event: E) {
|
||||
viewModelScope.launch {
|
||||
_uiEvent.emit(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送状态
|
||||
* @param [newState] 新状态
|
||||
*/
|
||||
private fun sendState(newState: S.() -> S) {
|
||||
_uiState.value = uiState.value.newState()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理事件,更新状态
|
||||
* 减少事件
|
||||
* @param [state] S
|
||||
* @param [event] E
|
||||
*/
|
||||
private fun reduceEvent(state: S, event: E) {
|
||||
viewModelScope.launch {
|
||||
handleEvent(event, state)?.let { newState -> sendState { newState } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送效果
|
||||
* @param [effect] 影响
|
||||
*/
|
||||
protected fun sendEffect(effect: F) {
|
||||
viewModelScope.launch { _uiEffect.emit(effect) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 收集效果
|
||||
* @param [lifecycleState] 生命周期状态
|
||||
* @param [sideEffect] 副作用
|
||||
*/
|
||||
@Composable
|
||||
fun <S : UiState, E : UiEvent, F : UiEffect> BaseViewModel<S, E, F>.CollectSideEffect(
|
||||
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
|
||||
sideEffect: (suspend (sideEffect: F) -> Unit),
|
||||
) {
|
||||
val sideEffectFlow = this.uiEffect
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(sideEffectFlow, lifecycleOwner) {
|
||||
lifecycleOwner.lifecycle.repeatOnLifecycle(lifecycleState) {
|
||||
sideEffectFlow.collect { sideEffect(it) }
|
||||
}
|
||||
}
|
||||
}
|
56
app/src/main/java/com/taskttl/ui/viewmodel/IndexViewModel.kt
Normal file
56
app/src/main/java/com/taskttl/ui/viewmodel/IndexViewModel.kt
Normal file
@ -0,0 +1,56 @@
|
||||
package com.taskttl.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.taskttl.data.contract.IndexEffect
|
||||
import com.taskttl.data.contract.IndexEvent
|
||||
import com.taskttl.data.contract.IndexState
|
||||
import com.taskttl.data.repository.TaskRepository
|
||||
import com.taskttl.utils.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* @author Hsy
|
||||
* @date 2024/09/06
|
||||
* @constructor 创建[IndexViewModel]
|
||||
*/
|
||||
@HiltViewModel
|
||||
class IndexViewModel @Inject constructor(
|
||||
private val taskRepository: TaskRepository
|
||||
) : BaseViewModel<IndexState, IndexEvent, IndexEffect>() {
|
||||
override fun initialState(): IndexState = IndexState();
|
||||
|
||||
init {
|
||||
getTask()
|
||||
}
|
||||
|
||||
private fun getTask() {
|
||||
viewModelScope.launch {
|
||||
val uncompletedList = taskRepository.getUncompletedTask()
|
||||
val comparableList = taskRepository.getCompletedTask()
|
||||
Logger.error(uncompletedList.toString())
|
||||
Logger.error(comparableList.toString())
|
||||
sendEvent(IndexEvent.LoadData(uncompletedList, comparableList))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleEvent(event: IndexEvent, state: IndexState): IndexState {
|
||||
return when (event) {
|
||||
is IndexEvent.LoadData -> {
|
||||
return state.copy(
|
||||
isLoading = true,
|
||||
uncompletedList = event.uncompletedList,
|
||||
completedList = event.completedList
|
||||
)
|
||||
}
|
||||
|
||||
is IndexEvent.Completed -> {
|
||||
taskRepository.completedTask(event.id)
|
||||
getTask()
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.taskttl.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor (
|
||||
|
||||
) :ViewModel(){
|
||||
// val settingsUiState: StateFlow<SettingsUiState>
|
||||
// get() = userDataRepository.userData.map {
|
||||
// SettingsUiState.Success(it)
|
||||
// }.stateIn(
|
||||
// scope = viewModelScope,
|
||||
// initialValue = SettingsUiState.Loading,
|
||||
// started = SharingStarted.WhileSubscribed(5_000),
|
||||
// )
|
||||
|
||||
var shouldShowSettingsDialog by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun setShowSettingsDialog(shouldShow: Boolean) {
|
||||
shouldShowSettingsDialog = shouldShow
|
||||
}
|
||||
|
||||
// fun updateThemeConfig(appThemeType: AppThemeType) {
|
||||
// viewModelScope.launch {
|
||||
// userDataRepository.updateThemeConfig(appThemeType)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
sealed interface SettingsUiState {
|
||||
data object Loading : SettingsUiState
|
||||
// data class Success(val userData: UserData) : SettingsUiState
|
||||
}
|
194
app/src/main/java/com/taskttl/utils/DateUtil.kt
Normal file
194
app/src/main/java/com/taskttl/utils/DateUtil.kt
Normal file
@ -0,0 +1,194 @@
|
||||
package com.taskttl.utils
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 日期工具
|
||||
* @author devttl
|
||||
* @date 2024/08/14
|
||||
*/
|
||||
object DateUtil {
|
||||
var YYYY: String = "yyyy"
|
||||
var YYYY_MM: String = "yyyy-MM"
|
||||
var YYYYMM: String = "yyyyMM"
|
||||
var YYYY_MM_DD: String = "yyyy-MM-dd"
|
||||
var YYYYMMDD: String = "yyyyMMdd"
|
||||
var YYYYMMDDHHMMSS: String = "yyyyMMddHHmmss"
|
||||
var HHMMSSSSS: String = "HHmmssSSS"
|
||||
var YYYY_MM_DD_HH_MM_SS: String = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
/**
|
||||
* 获取旧今天开始时间戳 旧接口
|
||||
* @return [Long]
|
||||
*/
|
||||
private fun getTodayStartTimestampOld(): Long {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||
calendar.set(Calendar.MINUTE, 0)
|
||||
calendar.set(Calendar.SECOND, 0)
|
||||
calendar.set(Calendar.MILLISECOND, 0)
|
||||
return calendar.timeInMillis
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天开始时间戳
|
||||
* @return [Long]
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getTodayStartTimestamp(): Long {
|
||||
val todayStart = LocalDate.now().atStartOfDay(ZoneId.systemDefault())
|
||||
return todayStart.toEpochSecond() * 1000 // 将秒转换为毫秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天开始时间戳
|
||||
* @return [Long]
|
||||
*/
|
||||
fun getTodayStartLong(): Long {
|
||||
val todayStartTimestamp: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getTodayStartTimestamp()
|
||||
} else {
|
||||
getTodayStartTimestampOld()
|
||||
}
|
||||
return todayStartTimestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取明天开始时间戳 旧接口
|
||||
* @return [Long]
|
||||
*/
|
||||
private fun getTomorrowStartTimestampOld(): Long {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.add(Calendar.DAY_OF_YEAR, 1)
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||
calendar.set(Calendar.MINUTE, 0)
|
||||
calendar.set(Calendar.SECOND, 0)
|
||||
calendar.set(Calendar.MILLISECOND, 0)
|
||||
return calendar.timeInMillis
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取明天开始时间戳
|
||||
* @return [Long]
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getTomorrowStartTimestamp(): Long {
|
||||
val tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault())
|
||||
return tomorrowStart.toEpochSecond() * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取明天开始时间戳
|
||||
* @return [Long]
|
||||
*/
|
||||
fun getTomorrowStartLong(): Long {
|
||||
val tomorrowStartTimestamp: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getTomorrowStartTimestamp()
|
||||
} else {
|
||||
getTomorrowStartTimestampOld()
|
||||
}
|
||||
return tomorrowStartTimestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间戳 旧接口
|
||||
* @return [Long]
|
||||
*/
|
||||
private fun getTimestampOld(): Long {
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间戳
|
||||
* @return [Long]
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getTimestamp(): Long {
|
||||
return Instant.now().toEpochMilli()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间戳
|
||||
* @return [Long]
|
||||
*/
|
||||
fun getLong(): Long {
|
||||
val timestamp: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getTimestamp()
|
||||
} else {
|
||||
getTimestampOld()
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
|
||||
fun today(): String {
|
||||
val dateStr: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val today = LocalDate.now()
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
today.format(formatter)
|
||||
} else {
|
||||
val calendar = Calendar.getInstance()
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
sdf.format(calendar)
|
||||
}
|
||||
return dateStr
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间戳转换为日期字符串
|
||||
* @param [timestamp] 时间戳
|
||||
* @param [format] 格式
|
||||
* @return [String]
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun convertTimestampToDateString(
|
||||
timestamp: Long,
|
||||
format: String = YYYY_MM_DD_HH_MM_SS
|
||||
): String {
|
||||
val formatter = DateTimeFormatter.ofPattern(format)
|
||||
val dateTime =
|
||||
LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
|
||||
return dateTime.format(formatter)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间戳转换为日期字符串 旧接口
|
||||
* @param [timestamp] 时间戳
|
||||
* @param [format] 格式
|
||||
* @return [String]
|
||||
*/
|
||||
private fun convertTimestampToDateStringOld(
|
||||
timestamp: Long,
|
||||
format: String = YYYY_MM_DD_HH_MM_SS
|
||||
): String {
|
||||
val sdf = SimpleDateFormat(format, Locale.getDefault())
|
||||
val date = Date(timestamp)
|
||||
return sdf.format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间戳转换为日期 str
|
||||
* @param [timestamp] 时间戳
|
||||
* @param [format] 格式
|
||||
* @return [String]
|
||||
*/
|
||||
fun convertTimestampToDateStr(timestamp: Long, format: String = YYYY_MM_DD_HH_MM_SS): String {
|
||||
val dateStr: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
convertTimestampToDateString(timestamp, format)
|
||||
} else {
|
||||
convertTimestampToDateStringOld(timestamp, format)
|
||||
}
|
||||
return dateStr
|
||||
}
|
||||
|
||||
|
||||
}
|
51
app/src/main/java/com/taskttl/utils/Logger.kt
Normal file
51
app/src/main/java/com/taskttl/utils/Logger.kt
Normal file
@ -0,0 +1,51 @@
|
||||
package com.taskttl.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.taskttl.R
|
||||
import com.taskttl.data.constant.Constant
|
||||
|
||||
/**
|
||||
* @author devttl
|
||||
* @date 2024/08/24
|
||||
*/
|
||||
object Logger {
|
||||
private var TAG = ""
|
||||
fun initialize(context: Context) {
|
||||
TAG = context.getString(R.string.appName)
|
||||
}
|
||||
|
||||
fun debug(message: String) {
|
||||
if (message.isEmpty()) {
|
||||
return
|
||||
}
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
fun info(message: String) {
|
||||
if (message.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (Constant.PRINT_LOG) {
|
||||
Log.i(TAG, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun warn(message: String) {
|
||||
if (message.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (Constant.PRINT_LOG) {
|
||||
Log.w(TAG, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun error(message: String) {
|
||||
if (message.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (Constant.PRINT_LOG) {
|
||||
Log.e(TAG, message)
|
||||
}
|
||||
}
|
||||
}
|
52
app/src/main/java/com/taskttl/utils/ToastUtil.kt
Normal file
52
app/src/main/java/com/taskttl/utils/ToastUtil.kt
Normal file
@ -0,0 +1,52 @@
|
||||
package com.taskttl.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
|
||||
/**
|
||||
* 吐司工具类
|
||||
*
|
||||
* @author devttl
|
||||
* @date 2024/06/16
|
||||
*/
|
||||
object ToastUtil {
|
||||
private var appContext: Context? = null
|
||||
|
||||
fun initialize(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示短时间的吐司消息
|
||||
* @param message 要显示的消息
|
||||
*/
|
||||
fun showShortToast(message: String) {
|
||||
Toast.makeText(appContext, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示长时间的吐司消息
|
||||
* @param message 要显示的消息
|
||||
*/
|
||||
fun showLongToast(message: String) {
|
||||
Toast.makeText(appContext, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示短时间的吐司消息
|
||||
* @param resId 要显示的消息的资源ID
|
||||
*/
|
||||
fun showShortToast(@StringRes resId: Int) {
|
||||
Toast.makeText(appContext, resId, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示长时间的吐司消息
|
||||
* @param resId 要显示的消息的资源ID
|
||||
*/
|
||||
fun showLongToast(@StringRes resId: Int) {
|
||||
Toast.makeText(appContext, resId, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package com.todottl
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.todottl.ui.screen.TodoTTLScreen
|
||||
import com.todottl.ui.theme.TodoTTLTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
TodoTTLTheme {
|
||||
TodoTTLScreen()
|
||||
// Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
// Greeting(
|
||||
// name = "Android",
|
||||
// modifier = Modifier.padding(innerPadding)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
TodoTTLTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package com.todottl.data
|
||||
|
||||
import com.todottl.R
|
||||
|
||||
object Constant {
|
||||
// 图片列表
|
||||
val imageList = listOf(
|
||||
R.mipmap.task,
|
||||
R.mipmap.goal,
|
||||
R.mipmap.event
|
||||
)
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.todottl.data
|
||||
|
||||
object RouteConfig {
|
||||
const val INDEX = "index"
|
||||
const val ADD = "addScreen"
|
||||
const val EDIT = "addScreen/{id}"
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package com.todottl.ui.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavController
|
||||
|
||||
@Composable
|
||||
fun EditScreen(navController: NavController, id: Int = 0) {
|
||||
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
package com.todottl.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.todottl.R
|
||||
import com.todottl.data.Constant
|
||||
import com.todottl.data.RouteConfig
|
||||
import com.todottl.ui.theme.TodoTTLTheme
|
||||
|
||||
@Composable
|
||||
fun IndexScreen(navController: NavController) {
|
||||
var isButtonVisible by remember { mutableStateOf(true) }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// 根据滚动状态来更新按钮的可见性
|
||||
LaunchedEffect(remember { derivedStateOf { listState.firstVisibleItemIndex } }) {
|
||||
isButtonVisible = listState.firstVisibleItemIndex == 0
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF1F5F9))
|
||||
) {
|
||||
// 背景图
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(222.dp),
|
||||
contentScale = ContentScale.Crop,
|
||||
painter = painterResource(R.mipmap.header),
|
||||
contentDescription = "Header"
|
||||
)
|
||||
Text(
|
||||
"日期",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 36.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
"My Todo List",
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 96.dp)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 158.dp, start = 16.dp, end = 16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
LazyColumn(state = listState) {
|
||||
items(Constant.imageList) {
|
||||
TodoItem(it)
|
||||
if (Constant.imageList.indexOf(it) != Constant.imageList.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isButtonVisible) {
|
||||
Button(
|
||||
onClick = {
|
||||
navController.navigate(RouteConfig.ADD)
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(50.dp)
|
||||
) {
|
||||
Text(text = "Add New Task")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TodoItem(index: Int) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(80.dp)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (index % 2 == 1) 0.5f else 1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(index),
|
||||
modifier = Modifier.size(48.dp),
|
||||
contentDescription = ""
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
"Today",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"0/0",
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
Checkbox(
|
||||
checked = index % 2 == 1,
|
||||
modifier = Modifier.size(24.dp),
|
||||
onCheckedChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun IndexScreenPreview() {
|
||||
val navController = rememberNavController()
|
||||
TodoTTLTheme {
|
||||
IndexScreen(navController)
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package com.todottl.ui.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.todottl.data.RouteConfig
|
||||
|
||||
@Composable
|
||||
fun TodoTTLScreen() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = RouteConfig.INDEX) {
|
||||
composable(RouteConfig.INDEX) { IndexScreen(navController) }
|
||||
composable(RouteConfig.ADD) { AddScreen(navController) }
|
||||
composable("addScreen/{id}") { backStackEntry ->
|
||||
backStackEntry.arguments?.getInt("id")?.let { EditScreen(navController, it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package com.todottl.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
@ -1,34 +0,0 @@
|
||||
package com.todottl.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
9
app/src/main/res/drawable/icon_add.xml
Normal file
9
app/src/main/res/drawable/icon_add.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M588.8,435.2h358.4a76.8,76.8 0,1 1,0 153.6h-358.4v358.4a76.8,76.8 0,1 1,-153.6 0v-358.4h-358.4a76.8,76.8 0,1 1,0 -153.6h358.4v-358.4a76.8,76.8 0,1 1,153.6 0v358.4z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
BIN
app/src/main/res/mipmap-hdpi/assignment.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/assignment.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
@ -1,3 +1,13 @@
|
||||
<resources>
|
||||
<string name="app_name">TodoTTL</string>
|
||||
<string name="nav_index">首页</string>
|
||||
<string name="nav_add">添加</string>
|
||||
<string name="nav_my">我的</string>
|
||||
|
||||
<string name="app_name">任务 TTL</string>
|
||||
<string name="index_title">我的任务列表</string>
|
||||
<string name="add_new_task">添加新任务</string>
|
||||
<string name="not_task">没有任务</string>
|
||||
<string name="save">保存</string>
|
||||
|
||||
<string name="appName">TaskTTL</string>
|
||||
</resources>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.TodoTTL" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.TaskTTL" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
@ -1,17 +0,0 @@
|
||||
package com.todottl
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
@ -2,4 +2,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.hilt) apply false
|
||||
}
|
@ -1,22 +1,27 @@
|
||||
[versions]
|
||||
agp = "8.6.0"
|
||||
agp = "8.8.0"
|
||||
kotlin = "1.9.24"
|
||||
coreKtx = "1.13.1"
|
||||
coreKtx = "1.15.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.4"
|
||||
activityCompose = "1.9.1"
|
||||
composeBom = "2024.08.00"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.12.01"
|
||||
|
||||
navigation = "2.7.7"
|
||||
|
||||
splashscreen = "1.0.1"
|
||||
navigation = "2.8.5"
|
||||
ksp = "1.9.24-1.0.20"
|
||||
hilt = "2.52"
|
||||
hiltNavigation = "1.2.0"
|
||||
room = '2.6.1'
|
||||
lifecycle = "2.8.7"
|
||||
gson = "2.11.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
@ -27,10 +32,33 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
|
||||
# core
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }
|
||||
|
||||
# navigation
|
||||
androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
||||
|
||||
# Hilt
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
hilt-android-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" }
|
||||
|
||||
# Room
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||
|
||||
# ViewModel
|
||||
viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
|
||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Wed Sep 04 11:14:54 CST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -19,5 +19,5 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "TodoTTL"
|
||||
rootProject.name = "TaskTTL"
|
||||
include(":app")
|
||||
|
Loading…
Reference in New Issue
Block a user