This commit is contained in:
devttl 2025-01-12 21:42:08 +08:00
parent f06e79997a
commit 25b82ddd12
54 changed files with 2472 additions and 461 deletions

View File

@ -1,14 +1,16 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
} }
android { android {
namespace = "com.todottl" namespace = "com.taskttl"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.todottl" applicationId = "com.taskttl"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
@ -28,6 +30,14 @@ android {
"proguard-rules.pro" "proguard-rules.pro"
) )
} }
debug {
// 混淆开关
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_19 sourceCompatibility = JavaVersion.VERSION_19
@ -50,9 +60,6 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
@ -67,6 +74,27 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
// core
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.splashscreen)
// navigation // navigation
implementation(libs.androidx.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)
} }

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<application <application
android:name=".di.TaskApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@ -10,12 +11,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TodoTTL" android:theme="@style/Theme.TaskTTL"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.TodoTTL"> android:theme="@style/Theme.TaskTTL">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -0,0 +1,26 @@
package com.taskttl
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.taskttl.ui.TaskListApp
import com.taskttl.ui.rememberAppState
import com.taskttl.ui.theme.TaskTTLTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val appState = rememberAppState()
TaskTTLTheme {
TaskListApp(appState)
}
}
}
}

View File

@ -0,0 +1,31 @@
package com.taskttl.data
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
@OptIn(ExperimentalSharedTransitionApi::class)
val LocalNavHostSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
val LocalAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
fun NavGraphBuilder.composableWithCompositionLocal(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
composable(route = route, arguments = arguments) {
CompositionLocalProvider(
LocalAnimatedVisibilityScope provides this@composable
) {
content(it)
}
}
}

View File

@ -0,0 +1,22 @@
package com.taskttl.data.constant
import com.taskttl.R
object Constant {
// 图片列表
val imageList = listOf(
R.mipmap.task,
R.mipmap.goal,
R.mipmap.event
)
var APP_NAME = R.string.appName
const val BASE_URL = "https://api.taskttl.com/"
private const val DEFAULT_TRUE = true
const val DEFAULT_FALSE = false
/** 是否打印日志 */
const val PRINT_LOG = DEFAULT_TRUE
}

View File

@ -0,0 +1,36 @@
package com.taskttl.data.contract
import com.taskttl.data.local.entity.TaskEntity
/**
* 添加状态
* @author Hsy
* @date 2024/09/07
* @constructor 创建[AddState]
* @param [isLoading] 正在加载
*/
data class AddState(
val isLoading: Boolean = false,
val taskEntity: TaskEntity = TaskEntity(),
) : UiState
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[IndexEvent]
*/
sealed interface AddEvent : UiEvent {
data class AddTask(val item: TaskEntity) : AddEvent
data class LoadTask(val id: Int) : AddEvent
data class UpdateTitle(val title: String) : AddEvent
data class UpdateContent(val content: String) : AddEvent
}
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[IndexEffect]
*/
sealed interface AddEffect : UiEffect {
data class AddSuccess(val message: String) : AddEffect
}

View File

@ -0,0 +1,24 @@
package com.taskttl.data.contract
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[UiState]
*/
interface UiState
/**
* 用户界面事件
* @author devttl
* @date 2024/08/12
* @constructor 创建[UiEvent]
*/
interface UiEvent
/**
* 用户界面效果
* @author devttl
* @date 2024/08/12
* @constructor 创建[UiEffect]
*/
interface UiEffect

View File

@ -0,0 +1,38 @@
package com.taskttl.data.contract
import com.taskttl.data.local.entity.TaskEntity
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[IndexState]
* @param [isLoading]
*/
data class IndexState(
val isLoading: Boolean = false,
val uncompletedList: List<TaskEntity> = listOf(),
val completedList: List<TaskEntity> = listOf(),
) : UiState
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[IndexEvent]
*/
sealed interface IndexEvent : UiEvent {
data class LoadData(
val uncompletedList: List<TaskEntity>,
val completedList: List<TaskEntity>
) : IndexEvent
data class Completed(val id: Int) : IndexEvent
}
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[IndexEffect]
*/
sealed interface IndexEffect : UiEffect {
data class ShowToast(val message: String) : IndexEffect
}

View File

@ -0,0 +1,14 @@
package com.taskttl.data.ext
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@Composable
fun Modifier.cancelRipperClick(onClick: () -> Unit) = this.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onClick
)

View File

@ -0,0 +1,22 @@
package com.taskttl.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import com.taskttl.data.local.dao.TaskDao
import com.taskttl.data.local.entity.TaskEntity
/**
* 应用程序数据库
* @author Hsy
* @date 2024/09/06
* @constructor 创建[AppDatabase]
*/
@Database(entities = [TaskEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
/**
* 待办Dao
* @return [TaskDao]
*/
abstract fun taskDao(): TaskDao
}

View File

@ -0,0 +1,54 @@
package com.taskttl.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.taskttl.data.local.entity.TaskEntity
@Dao
interface TaskDao {
/**
* 获取全部
* @return [List<TaskEntity>]
*/
@Query("SELECT * FROM tasks")
suspend fun getAll(): List<TaskEntity>
/**
* 获取待办事项由id
* @param [id] id
* @return [TaskEntity]
*/
@Query("SELECT * FROM tasks WHERE id = :id")
suspend fun getTaskById(id: Int): TaskEntity
/**
* 获取未完成待办事项
* @return [List<TaskEntity>]
*/
@Query("SELECT * FROM tasks WHERE completed = 0")
suspend fun getUncompletedTask(): List<TaskEntity>
/**
* 获取完成状态待办事项
* @return [List<TaskEntity>]
*/
@Query("SELECT * FROM tasks WHERE completed = 1")
suspend fun getCompletedTask(): List<TaskEntity>
/**
* 获取完成状态待办事项
* @return [List<TaskEntity>]
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addTask(task: TaskEntity): Long
/**
* 完成待办事项
* @param [id] id
*/
@Query("UPDATE tasks SET completed = 1 WHERE id = :id")
suspend fun completedTask(id: Int)
}

View File

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

View File

@ -0,0 +1,54 @@
package com.taskttl.data.repository
import com.taskttl.data.local.dao.TaskDao
import com.taskttl.data.local.entity.TaskEntity
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TaskRepository @Inject constructor(
private val taskDao: TaskDao
) {
/**
* 从数据库获取任务
* @return [List<TaskEntity>]
*/
suspend fun getAll(): List<TaskEntity> {
return taskDao.getAll()
}
/**
* 获取待办事项由id
* @param [id] id
* @return [TaskEntity]
*/
suspend fun getTaskById(id: Int): TaskEntity {
return taskDao.getTaskById(id)
}
/**
* 获取未完成待办事项
* @return [List<TaskEntity>]
*/
suspend fun getUncompletedTask(): List<TaskEntity> {
return taskDao.getUncompletedTask()
}
/**
* 获取完成状态待办事项
* @return [List<TaskEntity>]
*/
suspend fun getCompletedTask(): List<TaskEntity> {
return taskDao.getCompletedTask()
}
suspend fun addTask(task: TaskEntity): Long {
return taskDao.addTask(task)
}
suspend fun completedTask(id: Int) {
taskDao.completedTask(id)
}
}

View File

@ -0,0 +1,30 @@
package com.taskttl.di
import android.content.Context
import androidx.room.Room
import com.taskttl.data.local.AppDatabase
import com.taskttl.data.local.dao.TaskDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DataBaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
name = "TaskTTL.db"
).fallbackToDestructiveMigration().build()
}
@Provides
fun provideTaskDao(db: AppDatabase): TaskDao = db.taskDao()
}

View File

@ -0,0 +1,26 @@
package com.taskttl.di
import android.app.Application
import android.content.Context
import com.taskttl.utils.Logger
import com.taskttl.utils.ToastUtil
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TaskApplication : Application() {
companion object {
lateinit var appContext: Context
private set
}
override fun onCreate() {
super.onCreate()
// 初始化应用上下文
appContext = applicationContext
// 初始化Logger
Logger.initialize(appContext)
// 初始化吐司工具类
ToastUtil.initialize(appContext)
}
}

View File

@ -0,0 +1,289 @@
package com.taskttl.ui
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.taskttl.R
import com.taskttl.data.LocalAnimatedVisibilityScope
import com.taskttl.data.LocalNavHostSharedTransitionScope
import com.taskttl.data.composableWithCompositionLocal
import com.taskttl.data.ext.cancelRipperClick
import com.taskttl.ui.navigation.MainNavHost
import com.taskttl.ui.navigation.MainTopNavItem
import com.taskttl.ui.theme.TaskTTLTheme
import com.taskttl.ui.viewmodel.SettingsViewModel
const val MAIN_ROUTE = "main"
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun TaskListApp(appState: TaskTTLAppState) {
val settingsViewModel = hiltViewModel<SettingsViewModel>()
SharedTransitionLayout {
CompositionLocalProvider(LocalNavHostSharedTransitionScope provides this) {
AppNavHost(appState) {
settingsViewModel.setShowSettingsDialog(true)
}
}
}
if (settingsViewModel.shouldShowSettingsDialog) {
// SettingsDialog {
// settingsViewModel.setShowSettingsDialog(false)
// }
}
}
@Composable
private fun AppNavHost(appState: TaskTTLAppState, showSettingDialog: () -> Unit) {
NavHost(
navController = appState.navController,
startDestination = MAIN_ROUTE
) {
composableWithCompositionLocal(MAIN_ROUTE) {
MainRoute(
mainAppState = rememberMainState(
rememberNavController(), appState.snackBarHostState
),
settingClick = {
showSettingDialog()
},
// navigateToContentDetail = {
// appState.navigateToContentDetail(it)
// },
changeStatusBarIconMode = {
appState.iconIsLight = it
}
)
}
// homeDetailScreen {
// appState.navController.popBackStack()
// }
}
}
@Composable
fun MainRoute(
mainAppState: MainAppState,
settingClick: () -> Unit,
// navigateToContentDetail: (ContentBean) -> Unit,
changeStatusBarIconMode: (Boolean) -> Unit
) {
Scaffold(
contentWindowInsets = WindowInsets(0, 0, 0, 0), // 这里这样写原因是"我的"页面是沉浸式
containerColor = TaskTTLTheme.colors.background,
snackbarHost = {
SnackbarHost(hostState = mainAppState.snackBarHostState)
},
topBar = {
},
bottomBar = {
TaskTTLBottomBar(
mainAppState.mainTopLevelDestinations,
mainAppState::navigateToMainTopLevelDestination,
mainAppState.currentDestination,
changeStatusBarIconMode
) {
}
}
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
MainNavHost(
appState = mainAppState,
onSettingClick = settingClick,
// navigateToContentDetail = navigateToContentDetail
)
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun TaskTTLBottomBar(
topLevelDestinations: List<MainTopNavItem>,
navigateToTopLevelDestination: (MainTopNavItem, (Boolean) -> Unit) -> Unit,
currentDestination: NavDestination?,
changeStatusBarIconMode: (Boolean) -> Unit,
onClick: () -> Unit
) {
val sharedTransitionScope = LocalNavHostSharedTransitionScope.current ?: return
val animatedContentScope = LocalAnimatedVisibilityScope.current ?: return
with(animatedContentScope) {
with(sharedTransitionScope) {
Row(
modifier = Modifier
.renderInSharedTransitionScopeOverlay(
zIndexInOverlay = 1f,
)
.animateEnterExit(
enter = fadeIn(),
exit = fadeOut()
)
.height(60.dp)
.fillMaxWidth()
.background(TaskTTLTheme.colors.background)
.windowInsetsPadding(WindowInsets.navigationBars),
verticalAlignment = Alignment.CenterVertically
) {
topLevelDestinations.forEach {
val selected = currentDestination?.route == it.route
when (it) {
MainTopNavItem.ADD -> {
Box(
modifier = Modifier
.weight(1f),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.background(Color.Red, RoundedCornerShape(8.dp))
.width(48.dp)
.height(30.dp)
.cancelRipperClick {
navigateToTopLevelDestination(
it,
changeStatusBarIconMode
)
},
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.icon_add),
contentDescription = stringResource(it.titleResId),
tint = Color.White
)
}
}
}
else -> {
TaskTTLBottomItemText(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
appNavItem = it,
isSelected = selected,
messageNum = if (MainTopNavItem.MY == it) 18 else null
) {
navigateToTopLevelDestination(it, changeStatusBarIconMode)
}
}
}
}
}
}
}
}
fun Modifier.notificationDot(num: Int) =
composed {
val color = TaskTTLTheme.colors.theme
val textMeasure = rememberTextMeasurer()
drawWithContent {
drawContent()
val centerOffset = Offset(size.width - 3.dp.toPx(), 3.dp.toPx())
drawCircle(
color = Color.Red,
radius = 6.dp.toPx(),
center = centerOffset
)
val textLayoutResult = textMeasure.measure(
AnnotatedString(if (num < 100) "$num" else "99+"),
TextStyle(fontSize = 7.sp, color = Color.White)
)
drawText(
textLayoutResult,
topLeft = Offset(
centerOffset.x - textLayoutResult.size.width / 2f,
centerOffset.y - textLayoutResult.size.height / 2f
)
)
}
}
/**
* 底部导航栏文本
*/
@Composable
private fun TaskTTLBottomItemText(
modifier: Modifier = Modifier,
appNavItem: MainTopNavItem,
isSelected: Boolean,
messageNum: Int? = null,
onClick: () -> Unit
) {
Box(
modifier = modifier.cancelRipperClick {
onClick()
}, contentAlignment = Alignment.Center
) {
val fontSize by animateFloatAsState(
targetValue = if (isSelected) 16.sp.value else 14.sp.value,
animationSpec = tween(durationMillis = 100, easing = LinearEasing),
label = "tabTextSize"
)
Text(
modifier = if (messageNum != null) Modifier.notificationDot(messageNum) else Modifier,
text = stringResource(appNavItem.titleResId),
color = if (isSelected) TaskTTLTheme.colors.title else TaskTTLTheme.colors.body,
lineHeight = 16.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
fontSize = fontSize.sp
)
}
}

View File

@ -0,0 +1,108 @@
package com.taskttl.ui
import android.net.Uri
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.google.gson.Gson
import com.taskttl.data.local.entity.TaskEntity
import com.taskttl.ui.navigation.MainTopNavItem
import com.taskttl.ui.screen.TASK_DETAIL_ROUTE
import com.taskttl.ui.screen.navigateToAdd
import com.taskttl.ui.screen.navigateToIndex
import com.taskttl.ui.screen.navigateToMy
@Composable
fun rememberAppState(
navController: NavHostController = rememberNavController(),
snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }
): TaskTTLAppState {
return remember(navController) {
TaskTTLAppState(
navController = navController,
snackBarHostState = snackBarHostState
)
}
}
@Stable
class TaskTTLAppState(
val navController: NavHostController,
val snackBarHostState: SnackbarHostState
) {
// 默认状态栏图标颜色是深色
var iconIsLight by mutableStateOf(false)
val currentDestination: NavDestination?
@Composable get() = navController.currentBackStackEntryAsState().value?.destination
fun navigateToContentDetail(taskBean: TaskEntity) {
navController.navigate(
"$TASK_DETAIL_ROUTE/${Uri.encode(Gson().toJson(taskBean))}"
)
}
}
@Composable
fun rememberMainState(
navController: NavHostController = rememberNavController(),
snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }
): MainAppState {
return remember(navController) {
MainAppState(
navController = navController,
snackBarHostState = snackBarHostState
)
}
}
@Stable
class MainAppState(
val navController: NavHostController,
val snackBarHostState: SnackbarHostState
) {
val mainTopLevelDestinations: List<MainTopNavItem> = MainTopNavItem.entries
val currentDestination: NavDestination?
@Composable get() = navController.currentBackStackEntryAsState().value?.destination
fun navigateToMainTopLevelDestination(
topLevelDestination: MainTopNavItem, changeStatusBarIconMode: (Boolean) -> Unit
) {
val topLevelNavOptions = navOptions {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
when (topLevelDestination) {
MainTopNavItem.INDEX -> {
navController.navigateToIndex(topLevelNavOptions)
changeStatusBarIconMode(false)
}
MainTopNavItem.ADD -> {
navController.navigateToAdd(topLevelNavOptions)
changeStatusBarIconMode(true)
}
MainTopNavItem.MY -> {
navController.navigateToMy(topLevelNavOptions)
changeStatusBarIconMode(true)
}
}
}
}

View File

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

View File

@ -0,0 +1,22 @@
package com.taskttl.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun FromLabel(title: String, top: Dp = 24.dp) {
Text(
title,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = Color.Black,
modifier = Modifier.padding(top = top, bottom = 8.dp)
)
}

View File

@ -0,0 +1,39 @@
package com.taskttl.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.taskttl.R
@Composable
fun NotTask() {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// 使用图标
Icon(
painter = painterResource(R.mipmap.assignment),
contentDescription = null,
modifier = Modifier.size(120.dp),
tint = Color.Gray
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = stringResource(R.string.not_task), color = Color.Gray)
}
}
}

View File

@ -0,0 +1,77 @@
package com.taskttl.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.taskttl.data.constant.Constant
import com.taskttl.data.contract.IndexEvent
import com.taskttl.data.local.entity.TaskEntity
import com.taskttl.ui.viewmodel.IndexViewModel
@Composable
fun TaskItem(
taskEntity: TaskEntity,
// navController: NavController,
indexViewModel: IndexViewModel = hiltViewModel()
) {
Row(
modifier = Modifier
.height(80.dp)
.padding(16.dp),
// .clickable { navController.navigate("${RouteConfig.EDIT}/${taskEntity.id}") },
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.weight(1f)
.alpha(if (taskEntity.completed) 0.5f else 1f),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(Constant.imageList[taskEntity.category]),
modifier = Modifier.size(48.dp),
contentDescription = ""
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
taskEntity.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
Text(
taskEntity.dueDate.toString(),
fontSize = 14.sp,
modifier = Modifier.alpha(0.7f)
)
}
}
Checkbox(
checked = taskEntity.completed,
modifier = Modifier.size(24.dp),
onCheckedChange = {
indexViewModel.sendEvent(IndexEvent.Completed(taskEntity.id))
},
)
}
}

View File

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

View File

@ -0,0 +1,39 @@
package com.taskttl.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.taskttl.ui.MainAppState
import com.taskttl.ui.screen.INDEX_ROUTE
import com.taskttl.ui.screen.addScreen
import com.taskttl.ui.screen.indexScreen
import com.taskttl.ui.screen.myScreen
@Composable
fun MainNavHost(
appState: MainAppState,
startDestination: String = INDEX_ROUTE,
onSettingClick: () -> Unit,
) {
val navController = rememberNavController()
NavHost(
navController = appState.navController,
startDestination = startDestination
) {
indexScreen(
appState.snackBarHostState,
// onSettingClick,
// navigateToDetail = navigateToContentDetail
)
addScreen(snackBarHostState = appState.snackBarHostState)
myScreen(snackBarHostState = appState.snackBarHostState)
// composable(INDEX_ROUTE) { IndexScreen(navController) }
// composable(
// "${RouteConfig.EDIT}/{id}",
// arguments = listOf(navArgument("id") { type = NavType.IntType })
// ) { backStackEntry ->
// backStackEntry.arguments?.getInt("id")?.let { AddScreen(navController, it) }
// }
}
}

View File

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

View File

@ -1,4 +1,4 @@
package com.todottl.ui.screen package com.taskttl.ui.screen
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background 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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.todottl.R import androidx.navigation.NavGraphBuilder
import com.todottl.data.Constant 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 @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) } 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( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -56,28 +113,16 @@ fun AddScreen(navController: NavController) {
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
contentDescription = "" contentDescription = ""
) )
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 24.dp), Text(
verticalAlignment = Alignment.CenterVertically stringResource(R.string.add_new_task),
) { fontSize = 30.sp,
IconButton(onClick = { navController.popBackStack() }) { fontWeight = FontWeight.Bold,
Image( color = Color.White,
painter = painterResource(id = R.mipmap.close), modifier = Modifier
modifier = Modifier.size(48.dp), .align(Alignment.TopCenter)
contentDescription = "" .padding(top = 36.dp)
) )
}
Text(
"Add New Task",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.padding(end = 24.dp)
.weight(1f),
textAlign = TextAlign.Center
)
}
Column( Column(
modifier = Modifier modifier = Modifier
@ -106,14 +151,22 @@ fun AddScreen(navController: NavController) {
"Task Title", fontSize = 16.sp, modifier = Modifier.alpha(0.7f) "Task Title", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
) )
}, },
value = "", value = state.taskEntity.title,
onValueChange = {}, onValueChange = {
addViewModel.sendEvent(AddEvent.UpdateTitle(it))
titleValid = it.isNotEmpty()
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(55.dp), .height(55.dp),
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
colors = colors colors = colors,
isError = !titleValid
) )
if (!titleValid) {
Text(text = "Please enter valid text", color = Color.Red)
}
Row( Row(
modifier = Modifier modifier = Modifier
.padding(top = 24.dp) .padding(top = 24.dp)
@ -123,8 +176,7 @@ fun AddScreen(navController: NavController) {
FromLabel("Category", 0.dp) FromLabel("Category", 0.dp)
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))
Constant.imageList.forEachIndexed { index, item -> Constant.imageList.forEachIndexed { index, item ->
var modifier = Modifier var modifier = Modifier.size(48.dp)
.size(48.dp)
if (categoryData == index) { if (categoryData == index) {
modifier = Modifier modifier = Modifier
@ -149,54 +201,28 @@ fun AddScreen(navController: NavController) {
} }
} }
} }
Row { FromLabel("Date")
Column(modifier = Modifier.weight(1f)) { TextField(placeholder = {
FromLabel("Date") Text(
TextField(placeholder = { "Date", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
Text( )
"Date", fontSize = 16.sp, modifier = Modifier.alpha(0.7f) },
) value = date,
}, onValueChange = {
value = "", date = it
onValueChange = {}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(55.dp), .height(55.dp),
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
colors = colors, colors = colors,
trailingIcon = { trailingIcon = {
Image( Image(
painter = painterResource(id = R.mipmap.calendar), painter = painterResource(id = R.mipmap.calendar),
contentDescription = "", contentDescription = "",
modifier = Modifier.size(20.dp) 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("Notes") FromLabel("Notes")
TextField( TextField(
placeholder = { placeholder = {
@ -204,20 +230,32 @@ fun AddScreen(navController: NavController) {
"Notes", fontSize = 16.sp, modifier = Modifier.alpha(0.7f) "Notes", fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
) )
}, },
value = "", value = state.taskEntity.content,
maxLines = 5, maxLines = 5,
onValueChange = {}, onValueChange = {
addViewModel.sendEvent(AddEvent.UpdateContent(it))
contentValid = it.isNotEmpty()
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(177.dp), .height(140.dp),
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
colors = colors colors = colors,
isError = !contentValid
) )
if (!contentValid) {
Text(text = "Please enter valid text", color = Color.Red)
}
} }
Button( Button(
onClick = { 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 modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
@ -226,18 +264,7 @@ fun AddScreen(navController: NavController) {
.height(56.dp), .height(56.dp),
shape = RoundedCornerShape(50.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)
)
}

View File

@ -0,0 +1,249 @@
package com.taskttl.ui.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.google.gson.Gson
import com.taskttl.R
import com.taskttl.data.composableWithCompositionLocal
import com.taskttl.data.ext.cancelRipperClick
import com.taskttl.data.local.entity.TaskEntity
import com.taskttl.ui.components.NotTask
import com.taskttl.ui.components.TaskItem
import com.taskttl.ui.viewmodel.IndexViewModel
import com.taskttl.utils.DateUtil
import com.taskttl.utils.Logger
/** 首页 */
const val INDEX_ROUTE = "index"
const val HOME_DETAIL_ROUTE = "home_detail"
fun NavController.navigateToIndex(navOptions: NavOptions) = navigate(INDEX_ROUTE, navOptions)
fun NavGraphBuilder.indexScreen(
snackBarHostState: SnackbarHostState,
// onSettingClick: () -> Unit,
// navigateToDetail: (ContentBean) -> Unit
) {
composable(INDEX_ROUTE) {
IndexScreen(
snackBarHostState,
// onSettingClick,
// navigateToDetail
)
}
}
fun NavGraphBuilder.homeDetailScreen(
navigateToHome: () -> Unit
) {
composableWithCompositionLocal(
route = "$HOME_DETAIL_ROUTE/{content}",
arguments = listOf(navArgument("content") { NavType.StringType })
) {
val contentBean = try {
Gson().fromJson(it.arguments?.getString("content") ?: "", TaskEntity::class.java)
} catch (e: Exception) {
null
}
HomeDetailRoute()
}
}
@Composable
fun HomeDetailRoute() {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Message")
}
}
/**
* 首页
* @param [navController] NAV 控制器
* @param [indexViewModel] 索引视图模型
*/
@Composable
fun IndexScreen(snackBarHostState: SnackbarHostState) {
val indexViewModel = hiltViewModel<IndexViewModel>();
val state by indexViewModel.uiState.collectAsState()
var isButtonVisible by remember { mutableStateOf(true) }
val listState = rememberLazyListState()
var backgroundHeight by remember { mutableStateOf(160.dp) }
val textTopPadding = remember(backgroundHeight) {
mutableStateOf(calculateTextTopPadding(backgroundHeight))
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
var dp = backgroundHeight + delta.dp
if (dp >= 160.dp) {
dp = 160.dp
}
backgroundHeight = maxOf(96.dp, dp) // 最小高度 100.dp
textTopPadding.value = calculateTextTopPadding(backgroundHeight)
return Offset.Zero
}
}
}
// 根据滚动状态来更新按钮的可见性
LaunchedEffect(remember { derivedStateOf { listState.firstVisibleItemIndex } }) {
isButtonVisible = listState.firstVisibleItemIndex == 0
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF1F5F9))
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(backgroundHeight),
shape = RoundedCornerShape(0.dp)
) {
Box() {
// 背景图
Image(
painter = painterResource(R.mipmap.header),
modifier = Modifier
.fillMaxWidth()
.height(backgroundHeight),
contentScale = ContentScale.Crop,
contentDescription = "Header"
)
if (textTopPadding.value > 50.dp) {
Text(
DateUtil.today(),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 36.dp)
)
}
Text(
stringResource(R.string.index_title),
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = textTopPadding.value)
)
}
}
if (state.uncompletedList.isEmpty() && state.completedList.isEmpty()) {
NotTask()
} else {
Card(
modifier = Modifier
// .align(Alignment.TopCenter)
.padding(all = 10.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
shape = RoundedCornerShape(16.dp)
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
items(state.uncompletedList) {
TaskItem(it)
if (state.uncompletedList.indexOf(it) != state.uncompletedList.lastIndex) {
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
)
}
}
}
}
}
if (isButtonVisible) {
Row(
modifier = Modifier
// .align(Alignment.BottomCenter)
.padding(start = 16.dp, end = 16.dp, bottom = 24.dp)
.fillMaxWidth()
.height(56.dp)
.cancelRipperClick {
// navigateToDetail(contentBean)
// navController.navigate(RouteConfig.ADD)
},
// shape = RoundedCornerShape(50.dp)
) {
Text(text = stringResource(R.string.add_new_task))
}
}
}
}
// 计算 Text 组件的顶部偏移
fun calculateTextTopPadding(currentBackgroundHeight: Dp): Dp {
// 这里可以根据 backgroundHeight 的不同值返回不同的顶部偏移量
// 例如,保持文本始终位于背景图像的中心或某个固定位置。
return when (currentBackgroundHeight.value) {
in 96.0..160.0 -> (96 + (currentBackgroundHeight.value - 160)).dp
else -> 96.dp // 默认偏移
}
}

View File

@ -0,0 +1,67 @@
package com.taskttl.ui.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.taskttl.R
const val MY_ROUTE = "my"
fun NavController.navigateToMy(navOptions: NavOptions) = navigate(MY_ROUTE, navOptions)
fun NavGraphBuilder.myScreen(
snackBarHostState: SnackbarHostState,
// navigateToContentDetail: (ContentBean) -> Unit
) {
composable(route = MY_ROUTE) {
MyRoute(snackBarHostState)
}
}
@Composable
fun MyRoute(snackBarHostState: SnackbarHostState) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF1F5F9))
) {
Image(
painter = painterResource(id = R.mipmap.add_header),
modifier = Modifier
.fillMaxWidth()
.height(96.dp),
contentScale = ContentScale.Crop,
contentDescription = ""
)
Text(
stringResource(R.string.nav_my),
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 36.dp)
)
}
}

View File

@ -0,0 +1,50 @@
package com.taskttl.ui.theme
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val RedBookRed = Color(0xFFFF2E4D)
val WhiteWindow = Color(0xFFF5F6F7)
val WhiteBackground = Color(0xFFFFFFFF)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val BlackWindow = Color(0xFF111111)
val BlackBackground = Color(0xFF1F1D1D)
data class LorenColors(
val theme:Color,
val window: Color,
val background: Color,
val title: Color,
val body: Color,
val icon: Color,
val divider: Color
)
val lightLorenColors = LorenColors(
theme = RedBookRed,
window = WhiteWindow,
background = WhiteBackground,
title = Color.Black,
body = Color(0xFF666666),
icon = Color.Black,
divider = Color.LightGray
)
val darkLorenColors = LorenColors(
theme = RedBookRed,
window = BlackWindow,
background = BlackBackground,
title = Color.White,
body = Color(0xFF666666),
icon = Color.White,
divider = Color.DarkGray
)
val LocalCustomColors = staticCompositionLocalOf { lightLorenColors }

View File

@ -1,6 +1,5 @@
package com.todottl.ui.theme package com.taskttl.ui.theme
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -34,7 +33,7 @@ private val LightColorScheme = lightColorScheme(
) )
@Composable @Composable
fun TodoTTLTheme( fun TaskTTLTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
@ -56,3 +55,12 @@ fun TodoTTLTheme(
content = content content = content
) )
} }
object TaskTTLTheme {
val colors: LorenColors
@Composable
get() = LocalCustomColors.current
val textStyle: LorenTextStyle
@Composable
get() = LocalTextStyles.current
}

View File

@ -0,0 +1,88 @@
package com.taskttl.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@Immutable
data class LorenTextStyle(
val titleLarge: TextStyle,
val titleMedium: TextStyle,
val titleSmall: TextStyle,
val bodyLarge: TextStyle,
val bodyMedium: TextStyle,
val bodySmall: TextStyle,
)
val defaultTextStyle = LorenTextStyle(
titleLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp,
),
titleMedium = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
lineHeight = 24.sp,
letterSpacing = 0.1.sp,
),
titleSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp,
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp,
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp,
)
)
val LocalTextStyles = staticCompositionLocalOf { defaultTextStyle }

View File

@ -0,0 +1,46 @@
package com.taskttl.ui.viewmodel
import com.taskttl.data.contract.AddEffect
import com.taskttl.data.contract.AddEvent
import com.taskttl.data.contract.AddState
import com.taskttl.data.repository.TaskRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[AddViewModel]
*/
@HiltViewModel
class AddViewModel @Inject constructor(
private val taskRepository: TaskRepository
) : BaseViewModel<AddState, AddEvent, AddEffect>() {
override fun initialState(): AddState = AddState();
override suspend fun handleEvent(event: AddEvent, state: AddState): AddState {
return when (event) {
is AddEvent.AddTask -> {
val id = taskRepository.addTask(event.item)
if (id > 0) {
sendEffect(AddEffect.AddSuccess("任务添加成功"))
}
state.copy(isLoading = false)
}
is AddEvent.LoadTask -> {
val task = taskRepository.getTaskById(event.id)
state.copy(taskEntity = task)
}
is AddEvent.UpdateTitle -> {
state.copy(taskEntity = state.taskEntity.copy(title = event.title))
}
is AddEvent.UpdateContent -> {
state.copy(taskEntity = state.taskEntity.copy(content = event.content))
}
}
}
}

View File

@ -0,0 +1,132 @@
package com.taskttl.ui.viewmodel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import com.taskttl.data.contract.UiEffect
import com.taskttl.data.contract.UiEvent
import com.taskttl.data.contract.UiState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
* 基础视图模型
* @author devttl
* @date 2024/08/12
* @constructor 创建[BaseViewModel]
*/
abstract class BaseViewModel<S : UiState, E : UiEvent, F : UiEffect> : ViewModel() {
/** 初始状态 */
private val initialState: S by lazy { initialState() }
/**
* 初始状态
* @return [S]
*/
protected abstract fun initialState(): S
/** 用户界面状态 */
private val _uiState: MutableStateFlow<S> by lazy { MutableStateFlow(initialState) }
/** 用户界面状态 */
val uiState: StateFlow<S> by lazy { _uiState }
/** 用户界面事件 */
private val _uiEvent: MutableSharedFlow<E> = MutableSharedFlow()
/** 用户界面效果 */
private val _uiEffect: MutableSharedFlow<F> = MutableSharedFlow()
/** 用户界面效果 */
val uiEffect: Flow<F> = _uiEffect
/**
* 处理事件
* @param [event] 事件
* @param [state] 状态
* @return [S?]
*/
protected abstract suspend fun handleEvent(event: E, state: S): S?
init {
subscribeEvents()
}
/**
* 收集事件
*/
private fun subscribeEvents() {
viewModelScope.launch {
_uiEvent.collect {
reduceEvent(_uiState.value, it)
}
}
}
/**
* 发送事件
* @param [event] 事件
*/
fun sendEvent(event: E) {
viewModelScope.launch {
_uiEvent.emit(event)
}
}
/**
* 发送状态
* @param [newState] 新状态
*/
private fun sendState(newState: S.() -> S) {
_uiState.value = uiState.value.newState()
}
/**
* 处理事件更新状态
* 减少事件
* @param [state] S
* @param [event] E
*/
private fun reduceEvent(state: S, event: E) {
viewModelScope.launch {
handleEvent(event, state)?.let { newState -> sendState { newState } }
}
}
/**
* 发送效果
* @param [effect] 影响
*/
protected fun sendEffect(effect: F) {
viewModelScope.launch { _uiEffect.emit(effect) }
}
}
/**
* 收集效果
* @param [lifecycleState] 生命周期状态
* @param [sideEffect] 副作用
*/
@Composable
fun <S : UiState, E : UiEvent, F : UiEffect> BaseViewModel<S, E, F>.CollectSideEffect(
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
sideEffect: (suspend (sideEffect: F) -> Unit),
) {
val sideEffectFlow = this.uiEffect
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(sideEffectFlow, lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(lifecycleState) {
sideEffectFlow.collect { sideEffect(it) }
}
}
}

View File

@ -0,0 +1,56 @@
package com.taskttl.ui.viewmodel
import androidx.lifecycle.viewModelScope
import com.taskttl.data.contract.IndexEffect
import com.taskttl.data.contract.IndexEvent
import com.taskttl.data.contract.IndexState
import com.taskttl.data.repository.TaskRepository
import com.taskttl.utils.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* @author Hsy
* @date 2024/09/06
* @constructor 创建[IndexViewModel]
*/
@HiltViewModel
class IndexViewModel @Inject constructor(
private val taskRepository: TaskRepository
) : BaseViewModel<IndexState, IndexEvent, IndexEffect>() {
override fun initialState(): IndexState = IndexState();
init {
getTask()
}
private fun getTask() {
viewModelScope.launch {
val uncompletedList = taskRepository.getUncompletedTask()
val comparableList = taskRepository.getCompletedTask()
Logger.error(uncompletedList.toString())
Logger.error(comparableList.toString())
sendEvent(IndexEvent.LoadData(uncompletedList, comparableList))
}
}
override suspend fun handleEvent(event: IndexEvent, state: IndexState): IndexState {
return when (event) {
is IndexEvent.LoadData -> {
return state.copy(
isLoading = true,
uncompletedList = event.uncompletedList,
completedList = event.completedList
)
}
is IndexEvent.Completed -> {
taskRepository.completedTask(event.id)
getTask()
state
}
}
}
}

View File

@ -0,0 +1,40 @@
package com.taskttl.ui.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor (
) :ViewModel(){
// val settingsUiState: StateFlow<SettingsUiState>
// get() = userDataRepository.userData.map {
// SettingsUiState.Success(it)
// }.stateIn(
// scope = viewModelScope,
// initialValue = SettingsUiState.Loading,
// started = SharingStarted.WhileSubscribed(5_000),
// )
var shouldShowSettingsDialog by mutableStateOf(false)
private set
fun setShowSettingsDialog(shouldShow: Boolean) {
shouldShowSettingsDialog = shouldShow
}
// fun updateThemeConfig(appThemeType: AppThemeType) {
// viewModelScope.launch {
// userDataRepository.updateThemeConfig(appThemeType)
// }
// }
}
sealed interface SettingsUiState {
data object Loading : SettingsUiState
// data class Success(val userData: UserData) : SettingsUiState
}

View File

@ -0,0 +1,194 @@
package com.taskttl.utils
import android.os.Build
import androidx.annotation.RequiresApi
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
import java.util.Locale
/**
* 日期工具
* @author devttl
* @date 2024/08/14
*/
object DateUtil {
var YYYY: String = "yyyy"
var YYYY_MM: String = "yyyy-MM"
var YYYYMM: String = "yyyyMM"
var YYYY_MM_DD: String = "yyyy-MM-dd"
var YYYYMMDD: String = "yyyyMMdd"
var YYYYMMDDHHMMSS: String = "yyyyMMddHHmmss"
var HHMMSSSSS: String = "HHmmssSSS"
var YYYY_MM_DD_HH_MM_SS: String = "yyyy-MM-dd HH:mm:ss"
/**
* 获取旧今天开始时间戳 旧接口
* @return [Long]
*/
private fun getTodayStartTimestampOld(): Long {
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
return calendar.timeInMillis
}
/**
* 获取今天开始时间戳
* @return [Long]
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun getTodayStartTimestamp(): Long {
val todayStart = LocalDate.now().atStartOfDay(ZoneId.systemDefault())
return todayStart.toEpochSecond() * 1000 // 将秒转换为毫秒
}
/**
* 获取今天开始时间戳
* @return [Long]
*/
fun getTodayStartLong(): Long {
val todayStartTimestamp: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getTodayStartTimestamp()
} else {
getTodayStartTimestampOld()
}
return todayStartTimestamp
}
/**
* 获取明天开始时间戳 旧接口
* @return [Long]
*/
private fun getTomorrowStartTimestampOld(): Long {
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_YEAR, 1)
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
return calendar.timeInMillis
}
/**
* 获取明天开始时间戳
* @return [Long]
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun getTomorrowStartTimestamp(): Long {
val tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault())
return tomorrowStart.toEpochSecond() * 1000
}
/**
* 获取明天开始时间戳
* @return [Long]
*/
fun getTomorrowStartLong(): Long {
val tomorrowStartTimestamp: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getTomorrowStartTimestamp()
} else {
getTomorrowStartTimestampOld()
}
return tomorrowStartTimestamp
}
/**
* 获取时间戳 旧接口
* @return [Long]
*/
private fun getTimestampOld(): Long {
return System.currentTimeMillis()
}
/**
* 获取时间戳
* @return [Long]
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun getTimestamp(): Long {
return Instant.now().toEpochMilli()
}
/**
* 获取时间戳
* @return [Long]
*/
fun getLong(): Long {
val timestamp: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getTimestamp()
} else {
getTimestampOld()
}
return timestamp
}
fun today(): String {
val dateStr: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val today = LocalDate.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
today.format(formatter)
} else {
val calendar = Calendar.getInstance()
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
sdf.format(calendar)
}
return dateStr
}
/**
* 将时间戳转换为日期字符串
* @param [timestamp] 时间戳
* @param [format] 格式
* @return [String]
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun convertTimestampToDateString(
timestamp: Long,
format: String = YYYY_MM_DD_HH_MM_SS
): String {
val formatter = DateTimeFormatter.ofPattern(format)
val dateTime =
LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
return dateTime.format(formatter)
}
/**
* 将时间戳转换为日期字符串 旧接口
* @param [timestamp] 时间戳
* @param [format] 格式
* @return [String]
*/
private fun convertTimestampToDateStringOld(
timestamp: Long,
format: String = YYYY_MM_DD_HH_MM_SS
): String {
val sdf = SimpleDateFormat(format, Locale.getDefault())
val date = Date(timestamp)
return sdf.format(date)
}
/**
* 将时间戳转换为日期 str
* @param [timestamp] 时间戳
* @param [format] 格式
* @return [String]
*/
fun convertTimestampToDateStr(timestamp: Long, format: String = YYYY_MM_DD_HH_MM_SS): String {
val dateStr: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
convertTimestampToDateString(timestamp, format)
} else {
convertTimestampToDateStringOld(timestamp, format)
}
return dateStr
}
}

View File

@ -0,0 +1,51 @@
package com.taskttl.utils
import android.content.Context
import android.util.Log
import com.taskttl.R
import com.taskttl.data.constant.Constant
/**
* @author devttl
* @date 2024/08/24
*/
object Logger {
private var TAG = ""
fun initialize(context: Context) {
TAG = context.getString(R.string.appName)
}
fun debug(message: String) {
if (message.isEmpty()) {
return
}
Log.d(TAG, message)
}
fun info(message: String) {
if (message.isEmpty()) {
return
}
if (Constant.PRINT_LOG) {
Log.i(TAG, message)
}
}
fun warn(message: String) {
if (message.isEmpty()) {
return
}
if (Constant.PRINT_LOG) {
Log.w(TAG, message)
}
}
fun error(message: String) {
if (message.isEmpty()) {
return
}
if (Constant.PRINT_LOG) {
Log.e(TAG, message)
}
}
}

View File

@ -0,0 +1,52 @@
package com.taskttl.utils
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
/**
* 吐司工具类
*
* @author devttl
* @date 2024/06/16
*/
object ToastUtil {
private var appContext: Context? = null
fun initialize(context: Context) {
appContext = context.applicationContext
}
/**
* 显示短时间的吐司消息
* @param message 要显示的消息
*/
fun showShortToast(message: String) {
Toast.makeText(appContext, message, Toast.LENGTH_SHORT).show()
}
/**
* 显示长时间的吐司消息
* @param message 要显示的消息
*/
fun showLongToast(message: String) {
Toast.makeText(appContext, message, Toast.LENGTH_LONG).show()
}
/**
* 显示短时间的吐司消息
* @param resId 要显示的消息的资源ID
*/
fun showShortToast(@StringRes resId: Int) {
Toast.makeText(appContext, resId, Toast.LENGTH_SHORT).show()
}
/**
* 显示长时间的吐司消息
* @param resId 要显示的消息的资源ID
*/
fun showLongToast(@StringRes resId: Int) {
Toast.makeText(appContext, resId, Toast.LENGTH_LONG).show()
}
}

View File

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

View File

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

View File

@ -1,7 +0,0 @@
package com.todottl.data
object RouteConfig {
const val INDEX = "index"
const val ADD = "addScreen"
const val EDIT = "addScreen/{id}"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M588.8,435.2h358.4a76.8,76.8 0,1 1,0 153.6h-358.4v358.4a76.8,76.8 0,1 1,-153.6 0v-358.4h-358.4a76.8,76.8 0,1 1,0 -153.6h358.4v-358.4a76.8,76.8 0,1 1,153.6 0v358.4z"
android:fillColor="#ffffff"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +1,13 @@
<resources> <resources>
<string name="app_name">TodoTTL</string> <string name="nav_index">首页</string>
<string name="nav_add">添加</string>
<string name="nav_my">我的</string>
<string name="app_name">任务 TTL</string>
<string name="index_title">我的任务列表</string>
<string name="add_new_task">添加新任务</string>
<string name="not_task">没有任务</string>
<string name="save">保存</string>
<string name="appName">TaskTTL</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.TodoTTL" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.TaskTTL" parent="android:Theme.Material.Light.NoActionBar" />
</resources> </resources>

View File

@ -1,17 +0,0 @@
package com.todottl
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -2,4 +2,6 @@
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.hilt) apply false
} }

View File

@ -1,22 +1,27 @@
[versions] [versions]
agp = "8.6.0" agp = "8.8.0"
kotlin = "1.9.24" kotlin = "1.9.24"
coreKtx = "1.13.1" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.4" activityCompose = "1.9.3"
activityCompose = "1.9.1" composeBom = "2024.12.01"
composeBom = "2024.08.00"
navigation = "2.7.7"
splashscreen = "1.0.1"
navigation = "2.8.5"
ksp = "1.9.24-1.0.20"
hilt = "2.52"
hiltNavigation = "1.2.0"
room = '2.6.1'
lifecycle = "2.8.7"
gson = "2.11.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }
@ -27,10 +32,33 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
# core
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }
# navigation # navigation
androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-android-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" }
# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
# ViewModel
viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

View File

@ -1,6 +1,6 @@
#Wed Sep 04 11:14:54 CST 2024 #Wed Sep 04 11:14:54 CST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -19,5 +19,5 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "TodoTTL" rootProject.name = "TaskTTL"
include(":app") include(":app")