From 25b82ddd1281eb399e90b6b84eae829dc4917893 Mon Sep 17 00:00:00 2001 From: devttl Date: Sun, 12 Jan 2025 21:42:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 38 ++- app/src/main/AndroidManifest.xml | 5 +- app/src/main/java/com/taskttl/MainActivity.kt | 26 ++ .../data/LocalNavHostSharedTransitionScope.kt | 31 ++ .../com/taskttl/data/constant/Constant.kt | 22 ++ .../com/taskttl/data/contract/AddContract.kt | 36 +++ .../com/taskttl/data/contract/Contract.kt | 24 ++ .../taskttl/data/contract/IndexContract.kt | 38 +++ .../java/com/taskttl/data/ext/Modifier.kt | 14 + .../com/taskttl/data/local/AppDatabase.kt | 22 ++ .../com/taskttl/data/local/dao/TaskDao.kt | 54 ++++ .../taskttl/data/local/entity/TaskEntity.kt | 24 ++ .../taskttl/data/repository/TaskRepository.kt | 54 ++++ .../java/com/taskttl/di/DataBaseModule.kt | 30 ++ .../java/com/taskttl/di/TaskApplication.kt | 26 ++ .../main/java/com/taskttl/ui/TaskListApp.kt | 289 ++++++++++++++++++ .../java/com/taskttl/ui/TaskTTLAppState.kt | 108 +++++++ .../BaseVerificationCodeTextField.kt | 97 ++++++ .../com/taskttl/ui/components/FromLabel.kt | 22 ++ .../java/com/taskttl/ui/components/NotTask.kt | 39 +++ .../com/taskttl/ui/components/TaskItem.kt | 77 +++++ .../components/VerificationCodeTextField.kt | 129 ++++++++ .../com/taskttl/ui/navigation/MainNavHost.kt | 39 +++ .../taskttl/ui/navigation/MainTopNavItem.kt | 12 + .../ui/screen/AddScreen.kt | 221 ++++++++------ .../java/com/taskttl/ui/screen/IndexScreen.kt | 249 +++++++++++++++ .../java/com/taskttl/ui/screen/MyScreen.kt | 67 ++++ .../main/java/com/taskttl/ui/theme/Color.kt | 50 +++ .../{todottl => taskttl}/ui/theme/Theme.kt | 16 +- .../main/java/com/taskttl/ui/theme/Type.kt | 88 ++++++ .../com/taskttl/ui/viewmodel/AddViewModel.kt | 46 +++ .../com/taskttl/ui/viewmodel/BaseViewModel.kt | 132 ++++++++ .../taskttl/ui/viewmodel/IndexViewModel.kt | 56 ++++ .../taskttl/ui/viewmodel/SettingsViewModel.kt | 40 +++ .../main/java/com/taskttl/utils/DateUtil.kt | 194 ++++++++++++ app/src/main/java/com/taskttl/utils/Logger.kt | 51 ++++ .../main/java/com/taskttl/utils/ToastUtil.kt | 52 ++++ app/src/main/java/com/todottl/MainActivity.kt | 49 --- .../main/java/com/todottl/data/Constant.kt | 13 - .../main/java/com/todottl/data/RouteConfig.kt | 7 - .../java/com/todottl/ui/screen/EditScreen.kt | 9 - .../java/com/todottl/ui/screen/IndexScreen.kt | 181 ----------- .../java/com/todottl/ui/screen/TodoTTL.kt | 19 -- .../main/java/com/todottl/ui/theme/Color.kt | 11 - .../main/java/com/todottl/ui/theme/Type.kt | 34 --- app/src/main/res/drawable/icon_add.xml | 9 + app/src/main/res/mipmap-hdpi/assignment.png | Bin 0 -> 1042 bytes app/src/main/res/values/strings.xml | 12 +- app/src/main/res/values/themes.xml | 2 +- .../test/java/com/todottl/ExampleUnitTest.kt | 17 -- build.gradle.kts | 2 + gradle/libs.versions.toml | 46 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 2 +- 54 files changed, 2472 insertions(+), 461 deletions(-) create mode 100644 app/src/main/java/com/taskttl/MainActivity.kt create mode 100644 app/src/main/java/com/taskttl/data/LocalNavHostSharedTransitionScope.kt create mode 100644 app/src/main/java/com/taskttl/data/constant/Constant.kt create mode 100644 app/src/main/java/com/taskttl/data/contract/AddContract.kt create mode 100644 app/src/main/java/com/taskttl/data/contract/Contract.kt create mode 100644 app/src/main/java/com/taskttl/data/contract/IndexContract.kt create mode 100644 app/src/main/java/com/taskttl/data/ext/Modifier.kt create mode 100644 app/src/main/java/com/taskttl/data/local/AppDatabase.kt create mode 100644 app/src/main/java/com/taskttl/data/local/dao/TaskDao.kt create mode 100644 app/src/main/java/com/taskttl/data/local/entity/TaskEntity.kt create mode 100644 app/src/main/java/com/taskttl/data/repository/TaskRepository.kt create mode 100644 app/src/main/java/com/taskttl/di/DataBaseModule.kt create mode 100644 app/src/main/java/com/taskttl/di/TaskApplication.kt create mode 100644 app/src/main/java/com/taskttl/ui/TaskListApp.kt create mode 100644 app/src/main/java/com/taskttl/ui/TaskTTLAppState.kt create mode 100644 app/src/main/java/com/taskttl/ui/components/BaseVerificationCodeTextField.kt create mode 100644 app/src/main/java/com/taskttl/ui/components/FromLabel.kt create mode 100644 app/src/main/java/com/taskttl/ui/components/NotTask.kt create mode 100644 app/src/main/java/com/taskttl/ui/components/TaskItem.kt create mode 100644 app/src/main/java/com/taskttl/ui/components/VerificationCodeTextField.kt create mode 100644 app/src/main/java/com/taskttl/ui/navigation/MainNavHost.kt create mode 100644 app/src/main/java/com/taskttl/ui/navigation/MainTopNavItem.kt rename app/src/main/java/com/{todottl => taskttl}/ui/screen/AddScreen.kt (56%) create mode 100644 app/src/main/java/com/taskttl/ui/screen/IndexScreen.kt create mode 100644 app/src/main/java/com/taskttl/ui/screen/MyScreen.kt create mode 100644 app/src/main/java/com/taskttl/ui/theme/Color.kt rename app/src/main/java/com/{todottl => taskttl}/ui/theme/Theme.kt (86%) create mode 100644 app/src/main/java/com/taskttl/ui/theme/Type.kt create mode 100644 app/src/main/java/com/taskttl/ui/viewmodel/AddViewModel.kt create mode 100644 app/src/main/java/com/taskttl/ui/viewmodel/BaseViewModel.kt create mode 100644 app/src/main/java/com/taskttl/ui/viewmodel/IndexViewModel.kt create mode 100644 app/src/main/java/com/taskttl/ui/viewmodel/SettingsViewModel.kt create mode 100644 app/src/main/java/com/taskttl/utils/DateUtil.kt create mode 100644 app/src/main/java/com/taskttl/utils/Logger.kt create mode 100644 app/src/main/java/com/taskttl/utils/ToastUtil.kt delete mode 100644 app/src/main/java/com/todottl/MainActivity.kt delete mode 100644 app/src/main/java/com/todottl/data/Constant.kt delete mode 100644 app/src/main/java/com/todottl/data/RouteConfig.kt delete mode 100644 app/src/main/java/com/todottl/ui/screen/EditScreen.kt delete mode 100644 app/src/main/java/com/todottl/ui/screen/IndexScreen.kt delete mode 100644 app/src/main/java/com/todottl/ui/screen/TodoTTL.kt delete mode 100644 app/src/main/java/com/todottl/ui/theme/Color.kt delete mode 100644 app/src/main/java/com/todottl/ui/theme/Type.kt create mode 100644 app/src/main/res/drawable/icon_add.xml create mode 100644 app/src/main/res/mipmap-hdpi/assignment.png delete mode 100644 app/src/test/java/com/todottl/ExampleUnitTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 16c4ac5..bf74e7d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ad8936..34a2094 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + android:theme="@style/Theme.TaskTTL"> diff --git a/app/src/main/java/com/taskttl/MainActivity.kt b/app/src/main/java/com/taskttl/MainActivity.kt new file mode 100644 index 0000000..adf5abf --- /dev/null +++ b/app/src/main/java/com/taskttl/MainActivity.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/com/taskttl/data/LocalNavHostSharedTransitionScope.kt b/app/src/main/java/com/taskttl/data/LocalNavHostSharedTransitionScope.kt new file mode 100644 index 0000000..75172ed --- /dev/null +++ b/app/src/main/java/com/taskttl/data/LocalNavHostSharedTransitionScope.kt @@ -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 { null } +val LocalAnimatedVisibilityScope = compositionLocalOf { null } + +fun NavGraphBuilder.composableWithCompositionLocal( + route: String, + arguments: List = emptyList(), + content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit +) { + composable(route = route, arguments = arguments) { + CompositionLocalProvider( + LocalAnimatedVisibilityScope provides this@composable + ) { + content(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/constant/Constant.kt b/app/src/main/java/com/taskttl/data/constant/Constant.kt new file mode 100644 index 0000000..328c8d8 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/constant/Constant.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/contract/AddContract.kt b/app/src/main/java/com/taskttl/data/contract/AddContract.kt new file mode 100644 index 0000000..1be9e44 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/contract/AddContract.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/contract/Contract.kt b/app/src/main/java/com/taskttl/data/contract/Contract.kt new file mode 100644 index 0000000..77174d8 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/contract/Contract.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/contract/IndexContract.kt b/app/src/main/java/com/taskttl/data/contract/IndexContract.kt new file mode 100644 index 0000000..4fa31ee --- /dev/null +++ b/app/src/main/java/com/taskttl/data/contract/IndexContract.kt @@ -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 = listOf(), + val completedList: List = listOf(), +) : UiState + +/** + * @author Hsy + * @date 2024/09/06 + * @constructor 创建[IndexEvent] + */ +sealed interface IndexEvent : UiEvent { + data class LoadData( + val uncompletedList: List, + val completedList: List + ) : 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/ext/Modifier.kt b/app/src/main/java/com/taskttl/data/ext/Modifier.kt new file mode 100644 index 0000000..ab865b8 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/ext/Modifier.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/local/AppDatabase.kt b/app/src/main/java/com/taskttl/data/local/AppDatabase.kt new file mode 100644 index 0000000..dac2d51 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/local/AppDatabase.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/local/dao/TaskDao.kt b/app/src/main/java/com/taskttl/data/local/dao/TaskDao.kt new file mode 100644 index 0000000..bc320a9 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/local/dao/TaskDao.kt @@ -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] + */ + @Query("SELECT * FROM tasks") + suspend fun getAll(): List + + /** + * 获取待办事项由id + * @param [id] id + * @return [TaskEntity] + */ + @Query("SELECT * FROM tasks WHERE id = :id") + suspend fun getTaskById(id: Int): TaskEntity + + /** + * 获取未完成待办事项 + * @return [List] + */ + @Query("SELECT * FROM tasks WHERE completed = 0") + suspend fun getUncompletedTask(): List + + /** + * 获取完成状态待办事项 + * @return [List] + */ + @Query("SELECT * FROM tasks WHERE completed = 1") + suspend fun getCompletedTask(): List + + /** + * 获取完成状态待办事项 + * @return [List] + */ + @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) +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/local/entity/TaskEntity.kt b/app/src/main/java/com/taskttl/data/local/entity/TaskEntity.kt new file mode 100644 index 0000000..be9b1db --- /dev/null +++ b/app/src/main/java/com/taskttl/data/local/entity/TaskEntity.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/data/repository/TaskRepository.kt b/app/src/main/java/com/taskttl/data/repository/TaskRepository.kt new file mode 100644 index 0000000..a670e29 --- /dev/null +++ b/app/src/main/java/com/taskttl/data/repository/TaskRepository.kt @@ -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] + */ + suspend fun getAll(): List { + return taskDao.getAll() + } + + /** + * 获取待办事项由id + * @param [id] id + * @return [TaskEntity] + */ + suspend fun getTaskById(id: Int): TaskEntity { + return taskDao.getTaskById(id) + } + + /** + * 获取未完成待办事项 + * @return [List] + */ + suspend fun getUncompletedTask(): List { + return taskDao.getUncompletedTask() + } + + /** + * 获取完成状态待办事项 + * @return [List] + */ + suspend fun getCompletedTask(): List { + return taskDao.getCompletedTask() + } + + suspend fun addTask(task: TaskEntity): Long { + return taskDao.addTask(task) + } + + suspend fun completedTask(id: Int) { + taskDao.completedTask(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/di/DataBaseModule.kt b/app/src/main/java/com/taskttl/di/DataBaseModule.kt new file mode 100644 index 0000000..888de75 --- /dev/null +++ b/app/src/main/java/com/taskttl/di/DataBaseModule.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/di/TaskApplication.kt b/app/src/main/java/com/taskttl/di/TaskApplication.kt new file mode 100644 index 0000000..4239ad1 --- /dev/null +++ b/app/src/main/java/com/taskttl/di/TaskApplication.kt @@ -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) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/TaskListApp.kt b/app/src/main/java/com/taskttl/ui/TaskListApp.kt new file mode 100644 index 0000000..c820bac --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/TaskListApp.kt @@ -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() + + 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, + 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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/TaskTTLAppState.kt b/app/src/main/java/com/taskttl/ui/TaskTTLAppState.kt new file mode 100644 index 0000000..d53c582 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/TaskTTLAppState.kt @@ -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.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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/components/BaseVerificationCodeTextField.kt b/app/src/main/java/com/taskttl/ui/components/BaseVerificationCodeTextField.kt new file mode 100644 index 0000000..56bb957 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/components/BaseVerificationCodeTextField.kt @@ -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) + } + } + } + ) +} + diff --git a/app/src/main/java/com/taskttl/ui/components/FromLabel.kt b/app/src/main/java/com/taskttl/ui/components/FromLabel.kt new file mode 100644 index 0000000..3d0fbec --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/components/FromLabel.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/components/NotTask.kt b/app/src/main/java/com/taskttl/ui/components/NotTask.kt new file mode 100644 index 0000000..fa4e51a --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/components/NotTask.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/components/TaskItem.kt b/app/src/main/java/com/taskttl/ui/components/TaskItem.kt new file mode 100644 index 0000000..ae69f3d --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/components/TaskItem.kt @@ -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)) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/components/VerificationCodeTextField.kt b/app/src/main/java/com/taskttl/ui/components/VerificationCodeTextField.kt new file mode 100644 index 0000000..2847e59 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/components/VerificationCodeTextField.kt @@ -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 + // ) + // ) + } + } + } + } + } + } +} + diff --git a/app/src/main/java/com/taskttl/ui/navigation/MainNavHost.kt b/app/src/main/java/com/taskttl/ui/navigation/MainNavHost.kt new file mode 100644 index 0000000..55f1d20 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/navigation/MainNavHost.kt @@ -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) } +// } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/navigation/MainTopNavItem.kt b/app/src/main/java/com/taskttl/ui/navigation/MainTopNavItem.kt new file mode 100644 index 0000000..d0adf13 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/navigation/MainTopNavItem.kt @@ -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); +} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/screen/AddScreen.kt b/app/src/main/java/com/taskttl/ui/screen/AddScreen.kt similarity index 56% rename from app/src/main/java/com/todottl/ui/screen/AddScreen.kt rename to app/src/main/java/com/taskttl/ui/screen/AddScreen.kt index b38992c..ff12d47 100644 --- a/app/src/main/java/com/todottl/ui/screen/AddScreen.kt +++ b/app/src/main/java/com/taskttl/ui/screen/AddScreen.kt @@ -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) - ) } \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/screen/IndexScreen.kt b/app/src/main/java/com/taskttl/ui/screen/IndexScreen.kt new file mode 100644 index 0000000..4e2fbbc --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/screen/IndexScreen.kt @@ -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(); + 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 // 默认偏移 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/screen/MyScreen.kt b/app/src/main/java/com/taskttl/ui/screen/MyScreen.kt new file mode 100644 index 0000000..3feea25 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/screen/MyScreen.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/theme/Color.kt b/app/src/main/java/com/taskttl/ui/theme/Color.kt new file mode 100644 index 0000000..9e00f67 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/theme/Color.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/theme/Theme.kt b/app/src/main/java/com/taskttl/ui/theme/Theme.kt similarity index 86% rename from app/src/main/java/com/todottl/ui/theme/Theme.kt rename to app/src/main/java/com/taskttl/ui/theme/Theme.kt index 4d589b9..9beaec8 100644 --- a/app/src/main/java/com/todottl/ui/theme/Theme.kt +++ b/app/src/main/java/com/taskttl/ui/theme/Theme.kt @@ -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, @@ -55,4 +54,13 @@ fun TodoTTLTheme( typography = Typography, content = content ) -} \ No newline at end of file +} + +object TaskTTLTheme { + val colors: LorenColors + @Composable + get() = LocalCustomColors.current + val textStyle: LorenTextStyle + @Composable + get() = LocalTextStyles.current +} diff --git a/app/src/main/java/com/taskttl/ui/theme/Type.kt b/app/src/main/java/com/taskttl/ui/theme/Type.kt new file mode 100644 index 0000000..f54778d --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/theme/Type.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/viewmodel/AddViewModel.kt b/app/src/main/java/com/taskttl/ui/viewmodel/AddViewModel.kt new file mode 100644 index 0000000..be74779 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/viewmodel/AddViewModel.kt @@ -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() { + 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)) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/viewmodel/BaseViewModel.kt b/app/src/main/java/com/taskttl/ui/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..3a67646 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/viewmodel/BaseViewModel.kt @@ -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 : ViewModel() { + + /** 初始状态 */ + private val initialState: S by lazy { initialState() } + + /** + * 初始状态 + * @return [S] + */ + protected abstract fun initialState(): S + + /** 用户界面状态 */ + private val _uiState: MutableStateFlow by lazy { MutableStateFlow(initialState) } + + /** 用户界面状态 */ + val uiState: StateFlow by lazy { _uiState } + + /** 用户界面事件 */ + private val _uiEvent: MutableSharedFlow = MutableSharedFlow() + + /** 用户界面效果 */ + private val _uiEffect: MutableSharedFlow = MutableSharedFlow() + + /** 用户界面效果 */ + val uiEffect: Flow = _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 BaseViewModel.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) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/viewmodel/IndexViewModel.kt b/app/src/main/java/com/taskttl/ui/viewmodel/IndexViewModel.kt new file mode 100644 index 0000000..30d24e0 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/viewmodel/IndexViewModel.kt @@ -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() { + 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 + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/taskttl/ui/viewmodel/SettingsViewModel.kt new file mode 100644 index 0000000..fe53895 --- /dev/null +++ b/app/src/main/java/com/taskttl/ui/viewmodel/SettingsViewModel.kt @@ -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 +// 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/utils/DateUtil.kt b/app/src/main/java/com/taskttl/utils/DateUtil.kt new file mode 100644 index 0000000..b987d06 --- /dev/null +++ b/app/src/main/java/com/taskttl/utils/DateUtil.kt @@ -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 + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/utils/Logger.kt b/app/src/main/java/com/taskttl/utils/Logger.kt new file mode 100644 index 0000000..c985350 --- /dev/null +++ b/app/src/main/java/com/taskttl/utils/Logger.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/taskttl/utils/ToastUtil.kt b/app/src/main/java/com/taskttl/utils/ToastUtil.kt new file mode 100644 index 0000000..8264c56 --- /dev/null +++ b/app/src/main/java/com/taskttl/utils/ToastUtil.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/MainActivity.kt b/app/src/main/java/com/todottl/MainActivity.kt deleted file mode 100644 index 774f6c6..0000000 --- a/app/src/main/java/com/todottl/MainActivity.kt +++ /dev/null @@ -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") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/data/Constant.kt b/app/src/main/java/com/todottl/data/Constant.kt deleted file mode 100644 index dca5faa..0000000 --- a/app/src/main/java/com/todottl/data/Constant.kt +++ /dev/null @@ -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 - ) - -} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/data/RouteConfig.kt b/app/src/main/java/com/todottl/data/RouteConfig.kt deleted file mode 100644 index 872f0c1..0000000 --- a/app/src/main/java/com/todottl/data/RouteConfig.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.todottl.data - -object RouteConfig { - const val INDEX = "index" - const val ADD = "addScreen" - const val EDIT = "addScreen/{id}" -} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/screen/EditScreen.kt b/app/src/main/java/com/todottl/ui/screen/EditScreen.kt deleted file mode 100644 index 034f879..0000000 --- a/app/src/main/java/com/todottl/ui/screen/EditScreen.kt +++ /dev/null @@ -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) { - -} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/screen/IndexScreen.kt b/app/src/main/java/com/todottl/ui/screen/IndexScreen.kt deleted file mode 100644 index b8ebc8c..0000000 --- a/app/src/main/java/com/todottl/ui/screen/IndexScreen.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/screen/TodoTTL.kt b/app/src/main/java/com/todottl/ui/screen/TodoTTL.kt deleted file mode 100644 index 19124cd..0000000 --- a/app/src/main/java/com/todottl/ui/screen/TodoTTL.kt +++ /dev/null @@ -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) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/theme/Color.kt b/app/src/main/java/com/todottl/ui/theme/Color.kt deleted file mode 100644 index 456e404..0000000 --- a/app/src/main/java/com/todottl/ui/theme/Color.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/todottl/ui/theme/Type.kt b/app/src/main/java/com/todottl/ui/theme/Type.kt deleted file mode 100644 index 5157e3c..0000000 --- a/app/src/main/java/com/todottl/ui/theme/Type.kt +++ /dev/null @@ -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 - ) - */ -) \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_add.xml b/app/src/main/res/drawable/icon_add.xml new file mode 100644 index 0000000..5139aa6 --- /dev/null +++ b/app/src/main/res/drawable/icon_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/mipmap-hdpi/assignment.png b/app/src/main/res/mipmap-hdpi/assignment.png new file mode 100644 index 0000000000000000000000000000000000000000..05f670ad31fd1a740c197d55f3d853820191117b GIT binary patch literal 1042 zcmV+t1nv8YP)Px&%t=H+RCr$Po!@cWFbu}QbdhwEWRY5q`=Bk7Y@vrbmAgpYN$Nq;ut&GP+KMd* zBuG&1=es=Q2m<8yBlyR;kdE*%Rlu1>M{oiF<3Rub05DyFrfF~AZfFr|I7fFx`=8z6 z$9ekEzjXa{kM@?c$bf_J&e78Z$#&jT|LvuFo?vTkQaym?b9?7KeUDIT=Nv8jFWokx zZm}v=14t16AYk1}` zWsN(eDgjIx{od2l%^Ph3q_8!eutTac3z7jGZ4nRqa?OEz$ydKwcgy3##i|V%(-a8) z@7(c=p8LKFzej(P0DO9AA192w=*C^D=p5Y-x0hic_zx7;jgp__Ge%M4t-((R`b4C4uc9YSs(^baJOL~VRA_ES`Y(3^Msn*rU*m zG7(3cvsksN{TGX+-GDiXAKI$je*h4U000mQrfm)Y+KLMO0f102ZF2z7R#fN@0EB{R zo6jBq*8y$8r2e>b4A*|H{;)m8x1n4Iq=ISNa{_Yh=jsChLyQv;0JMkvz#jl)7MODn z0GvXZ%K;#>z?^#k;1tSSe)Rw{>zCdA>~}%g70Mg{05S{Axd#AFq0Hp~kXc~PJpgbD zWiAJR%mQ=n0f18|bNTE6a2?33AM5_=4`KjtItK6u8GIeh&I&~hFa;Hr`+YC~0Dy4? zxJ5K|1R3j3sqN?L5BrBj_l3JC3`Ba(BE4B!+(28`bo z!HpoHzN|SiU?33yoCSYkO8C9Wbq7OC+*4&xG7YcaR49$4gf;Iw9NrPTT!7u z01yhMZ4Ln1iVFP!fKV`Pa{$m*ROqh;0Jyky1X_u6fZ4V6!og}`{_a+Ht`Xvc7%uO= zENPL;yDPcAo*P&u<>wxj;2I*%fZ(u#2OPM56)%r{r6~$P=pC-HKVdAsw*F8%8bEM> z^?)d5-akf{1YkcM{dv9U@$aj5CXf0Qo^STgSb0y|@Q#`3GggH=ewE>pGvF#!DcC6h zC@A*1Pyhe`#tImRM+N|Zu>!{7kpTc;tblQNWB>pdD_|TR8DI?HHwCooe}^&|Y5)KL M07*qoM6N<$g0aBLxBvhE literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 194f38c..bc22377 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,13 @@ - TodoTTL + 首页 + 添加 + 我的 + + 任务 TTL + 我的任务列表 + 添加新任务 + 没有任务 + 保存 + + TaskTTL \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 9c6138d..b86351f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ -