diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..1a5f2d9
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..376e01c
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-ldpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-ldpi/ic_launcher.png
new file mode 100644
index 0000000..6056628
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-ldpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-ldpi/ic_launcher_round.png
new file mode 100644
index 0000000..94ffb1f
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-ldpi/ic_launcher_round.png differ
diff --git a/composeApp/src/commonMain/composeResources/values-zh/strings.xml b/composeApp/src/commonMain/composeResources/values-zh/strings.xml
new file mode 100644
index 0000000..584468e
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/values-zh/strings.xml
@@ -0,0 +1,316 @@
+
+
+ 继续
+ 跳过
+ 开始使用
+
+ 欢迎使用 TaskTTL
+ 一个简洁而强大的任务管理工具,帮助您高效管理日常任务和重要日期
+ 智能任务管理
+ 创建、分类和跟踪您的任务。设置优先级,添加截止日期,让工作更有条理
+ 重要日期提醒
+ 设置重要日期的倒数计时,永远不会错过生日、纪念日或重要的截止日期
+ 准备就绪!
+ 现在您可以开始创建第一个任务,让我们一起提高工作效率吧!
+
+
+ TaskTTL
+ 任务管理与倒数日应用
+ 让每一天都更有意义
+ 版本
+ 构建版本
+
+
+ 待办
+ 倒数日
+ 统计
+ 设置
+
+
+ 搜索任务...
+ 我的任务
+ 任务详情
+ 添加任务
+ 编辑任务
+
+ 任务列表
+ 显示已完成
+ 暂无任务
+ 点击右下角按钮添加新任务
+ 已完成
+ 未完成
+
+ 任务标题
+ 任务描述
+ 选择分类
+ 优先级
+ 截止日期(可选)
+ 选择日期
+ 标签(用逗号分隔)
+ 例如:重要,紧急,工作
+
+ 任务不存在
+ 截止日期:
+ 无
+ 创建时间:
+ 任务描述
+
+ 任务添加成功
+ 添加任务失败
+ 任务更新成功
+ 更新任务失败
+ 任务删除成功
+ 删除任务失败
+ 加载任务失败
+ 查询任务失败
+ 更新任务状态成功
+ 更新任务状态失败
+
+
+ 倒数日
+ 倒数日详情
+ 添加倒数日
+ 编辑倒数日
+
+ 倒数日列表
+ 天
+ 暂无倒数日
+ 点击右下角按钮添加新的倒数日
+ 添加倒数日
+
+ 倒数日标题
+ 倒数日描述
+ 目标日期
+ 通知设置
+ 倒数日不存在
+ 事件描述
+ 详细信息
+ 提醒
+
+ 倒数日添加成功
+ 添加倒数日失败
+ 倒数日更新成功
+ 更新倒数日失败
+ 倒数日删除成功
+ 删除倒数日失败
+ 加载倒数日失败
+ 查询倒数日失败
+
+
+ 统计
+ 总览
+ 分类统计
+ 总任务
+ 已完成
+ 完成率
+ 倒数日总数
+ 活跃中
+
+
+ 任务
+ 倒数日
+
+ 分类管理
+ 添加分类
+ 编辑分类
+ 暂无分类
+ 点击右下角按钮添加新分类
+ 分类名称
+ 输入分类名称...
+ 分类类型
+ 选择颜色
+ 选择图标
+ 任务分类
+ 倒数日分类
+
+ %1$d 个任务
+ %1$d 个倒数日
+
+
+ 分类添加成功
+ 添加分类失败
+ 分类更新成功
+ 更新分类失败
+ 分类删除成功
+ 删除分类失败
+ 加载分类失败
+ 加载统计数据失败
+ 默认分类初始化成功
+ 初始化默认分类失败
+ 查询分类失败
+ 更新分类计数失败
+
+
+ 进入
+ 编辑
+ 取消
+ 确定
+ 删除
+ 导出
+ 导入
+ 返回
+ 操作
+ 搜索
+ 清除
+ 全部
+ 重试
+ 选择文件
+ 错误
+ 正在加载...
+ 加载失败,请检查网络连接
+
+
+
+ 数据管理
+ 导出数据
+ 将所有任务和倒数日导出为文件
+ 导入数据
+ 从文件导入任务和倒数日
+ 自动备份
+ 定期自动备份数据到云端
+ 清除所有数据
+ 删除所有任务、倒数日和设置
+ 此操作将删除所有任务、倒数日和设置,且无法恢复。
+ 清理已完成任务
+ 清理过期倒数日
+ 删除所有已完成的任务
+ 删除所有已过期的倒数日
+
+ 备份与恢复
+ 数据清理
+ 选择要导入的文件
+ 选择文件
+ 选择导出格式
+ JSON格式
+ CSV格式
+
+
+ 应用设置
+ 通用设置
+ 数据管理
+ 社交分享
+ 帮助与反馈
+
+ 推送通知
+ 接收任务和倒数日提醒
+ 深色模式
+ 使用深色主题
+ 语言设置
+ 简体中文
+
+ 分类管理
+ 管理分类
+ 数据管理
+ 备份和恢复数据
+
+ 分享成就
+ 分享任务完成成就
+ 推荐给朋友
+ 邀请朋友使用 TaskMaster
+
+ 意见反馈
+ 告诉我们您的想法
+ 隐私政策
+ 了解我们如何保护您的数据
+ 应用评价
+ 如果喜欢,欢迎在商店留下五星好评
+ 关于应用
+ 100001
+ 1.0.1
+ 版本 1.0.1
+
+
+ 意见反馈
+ 反馈类型
+ 问题反馈
+ 功能建议
+ 问题描述
+ 请详细描述您遇到的问题或建议...
+ 联系方式(可选)
+ 您的邮箱地址,方便我们回复
+ 感谢您的反馈!我们会尽快处理。
+ 请填写反馈内容
+ 发送反馈
+
+
+ 关于
+ 应用介绍
+ TaskTTL 是一款现代化的任务管理与倒数日应用,\n支持分类管理、优先级设置与统计分析,让生活更有条理。
+
+ 隐私协议
+
+ 技术栈
+ Kotlin Multiplatform(跨平台开发框架)
+ Jetpack Compose(现代化 UI 框架)
+ Room Database(本地存储)
+ Koin(依赖注入框架)
+ Ktor(网络请求)
+ MVI Architecture(响应式架构模式)
+
+ 开发者
+ DevTTL 团队
+ 联系我们
+ 电子邮箱
+ mailto:%1$s
+ admin@devttl.com
+ 官方网站
+ https://devttl.com
+ 2025
+ 保留所有权利
+
+ 低
+ 中
+ 高
+ 紧急
+
+ 一次
+ 每天
+ 每周
+ 每月
+ 关闭
+
+
+ 工作
+ 学习
+ 考试
+ 项目
+
+
+ 家庭
+ 休闲
+ 购物
+ 美食
+ 家务
+
+
+ 健康
+ 健身
+ 作息
+
+
+ 音乐
+ 娱乐
+ 摄影
+ 影视
+
+
+ 出行
+ 旅行
+ 步行
+
+
+ 生日
+ 节日
+ 纪念日
+
+
+ 理财
+ 目标
+ 提醒
+
+ https://sites.google.com/view/taskttl/privacy
+
+ 反馈成功
+ 反馈失败,请检查网络连接或稍后重试
+
+
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/viewmodel/BaseViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/viewmodel/BaseViewModel.kt
new file mode 100644
index 0000000..4f83f45
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/viewmodel/BaseViewModel.kt
@@ -0,0 +1,97 @@
+package com.taskttl.core.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/**
+ * 基础视图模型
+ * @author DevTTL
+ * @date 2025/10/15
+ * @constructor 创建[BaseViewModel]
+ * @param [initialState] 初始状态
+ */
+abstract class BaseViewModel(initialState: S) : ViewModel() {
+
+ // 状态流
+ private val _state = MutableStateFlow(initialState)
+ val state: StateFlow = _state.asStateFlow()
+
+ // 事件流
+ private val _effects = MutableSharedFlow()
+ val effects: SharedFlow = _effects.asSharedFlow()
+
+ // 对外入口处理 Intent
+ fun processIntent(intent: I) {
+ viewModelScope.launch { handleIntent(intent) }
+ }
+
+ /**
+ * 处理意图的具体实现 - 子类必须实现此方法
+ * @param intent 意图
+ */
+ protected abstract fun handleIntent(intent: I)
+
+
+ // private fun launchWithProcessing(
+ // showLoading: Boolean = true,
+ // block: suspend () -> Unit,
+ // ) {
+ // viewModelScope.launch {
+ // val currentState = _state.value
+ // if (currentState.isProcessing) return@launch // 防重入
+ //
+ // // 设置状态
+ // updateState { copy(isLoading = showLoading, isProcessing = true) }
+ // try {
+ // block()
+ // } finally {
+ // updateState { copy(isLoading = false, isProcessing = false) }
+ // }
+ // }
+ // }
+
+
+ /**
+ * 发送单向事件
+ */
+ protected fun sendEvent(event: E) {
+ viewModelScope.launch { _effects.emit(event) }
+ }
+
+ /**
+ * 更新状态
+ */
+ protected fun updateState(reduce: S.() -> S) {
+ viewModelScope.launch { _state.update { it.reduce() } }
+ }
+
+ // /**
+ // * 清除错误
+ // */
+ // protected fun clearError() {
+ // _state.value = _state.value.copy(error = null)
+ // }
+}
+
+/**
+ * 基本ui状态
+ * @author DevTTL
+ * @date 2025/10/15
+ * @constructor 创建[BaseUiState]
+ * @param [isLoading] 正在加载
+ * @param [isProcessing] 正在处理
+ * @param [error] 错误
+ */
+open class BaseUiState(
+ open val isLoading: Boolean = false,
+ open val isProcessing: Boolean = false,
+ open val error: String? = null,
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CategoryState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CategoryState.kt
index 6119e42..64184fd 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CategoryState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CategoryState.kt
@@ -1,5 +1,6 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryStatistics
import com.taskttl.data.local.model.CategoryType
@@ -23,6 +24,9 @@ import com.taskttl.data.local.model.CategoryType
* @param [showDeleteDialog] 显示删除对话
*/
data class CategoryState(
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
val categories: List = emptyList(),
val editingCategory: Category? = null,
val taskCategories: List = emptyList(),
@@ -30,12 +34,10 @@ data class CategoryState(
val categoryStatistics: List = emptyList(),
val selectedCategory: Category? = null,
val selectedType: CategoryType = CategoryType.TASK,
- val isLoading: Boolean = false,
- val error: String? = null,
val showAddDialog: Boolean = false,
val showEditDialog: Boolean = false,
val showDeleteDialog: Boolean = false
-)
+): BaseUiState()
/**
* 类别意图
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CountdownState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CountdownState.kt
index ec9b133..fb6b08b 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CountdownState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CountdownState.kt
@@ -1,5 +1,6 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.Countdown
@@ -17,14 +18,15 @@ import com.taskttl.data.local.model.Countdown
* @param [error] 错误
*/
data class CountdownState(
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
val countdowns: List = emptyList(),
val categories: List = emptyList(),
val editingCountdown: Countdown? = null,
val filteredCountdowns: List = emptyList(),
val selectedCategory: Category? = null,
- val isLoading: Boolean = false,
- val error: String? = null
-)
+): BaseUiState()
/**
* 倒数日意图
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt
index ac4ff40..c9ebd16 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt
@@ -1,11 +1,13 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.network.domain.req.FeedbackReq
data class FeedbackState(
- val isLoading: Boolean = false,
- val error: String? = null
-)
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
+) : BaseUiState()
sealed class FeedbackIntent {
/**
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/OnboardingState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/OnboardingState.kt
index c9fd9ce..9016723 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/OnboardingState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/OnboardingState.kt
@@ -1,5 +1,6 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.OnboardingPage
/**
@@ -9,9 +10,11 @@ import com.taskttl.data.local.model.OnboardingPage
* @constructor 创建[OnboardingState]
*/
data class OnboardingState(
- val isLoading: Boolean = false,
- val pages: List
-)
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
+ val pages: List = OnboardingPage.entries,
+) : BaseUiState()
/**
* 引导意图
@@ -19,26 +22,29 @@ data class OnboardingState(
* @date 2025/09/06
* @constructor 创建[OnboardingIntent]
*/
-sealed class OnboardingIntent {}
+sealed class OnboardingIntent {
+ object NextPage : OnboardingIntent()
+ object MarkOnboardingCompleted : OnboardingIntent()
+}
/**
* 引导活动
* @author DevTTL
* @date 2025/09/06
- * @constructor 创建[OnboardingEvent]
+ * @constructor 创建[OnboardingEffect]
*/
-sealed class OnboardingEvent {
+sealed class OnboardingEffect {
/**
* 下一页
* @author DevTTL
* @date 2025/09/06
*/
- data object NextPage : OnboardingEvent()
+ data object NextPage : OnboardingEffect()
/**
* 导航Main
* @author admin
* @date 2025/10/05
*/
- data object NavMain : OnboardingEvent()
+ data object NavMain : OnboardingEffect()
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SettingsState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SettingsState.kt
index 6ab1314..5c49f50 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SettingsState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SettingsState.kt
@@ -1,5 +1,7 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
+
/**
* 设置状态
* @author DevTTL
@@ -9,9 +11,10 @@ package com.taskttl.data.state
* @param [error] 错误
*/
data class SettingsState(
- val isLoading: Boolean = false,
- val error: String? = null,
-)
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
+) : BaseUiState()
/**
* 设置意图
@@ -25,7 +28,7 @@ sealed class SettingsIntent {
* @author DevTTL
* @date 2025/10/14
*/
- object OpenAppRating: SettingsIntent()
+ object OpenAppRating : SettingsIntent()
/**
* 打开网址
@@ -34,7 +37,7 @@ sealed class SettingsIntent {
* @constructor 创建[OpenUrl]
* @param [url] 网址
*/
- class OpenUrl(val url:String): SettingsIntent()
+ class OpenUrl(val url: String) : SettingsIntent()
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SplashState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SplashState.kt
index d616ee2..8110fd3 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SplashState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/SplashState.kt
@@ -1,30 +1,37 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
+
/**
* 启动页状态
* @author admin
* @date 2025/10/05
* @constructor 创建[SplashState]
*/
-sealed interface SplashState {
- /**
- * 加载中
- * @author admin
- * @date 2025/08/11
- */
- data object Loading : SplashState
+data class SplashState(
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
+) : BaseUiState()
+
+sealed class SplashIntent {
+ object LoadApp : SplashIntent()
+}
+
+
+sealed class SplashEffect {
/**
* 导航到首页
* @author admin
* @date 2025/08/11
*/
- data object NavigateToMain : SplashState
+ data object NavigateToMain : SplashEffect()
/**
* 导航到引导页
* @author admin
* @date 2025/08/11
*/
- data object NavigateToOnboarding : SplashState
+ data object NavigateToOnboarding : SplashEffect()
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/TaskState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/TaskState.kt
index c652629..f16b440 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/TaskState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/TaskState.kt
@@ -1,5 +1,6 @@
package com.taskttl.data.state
+import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.Task
@@ -18,6 +19,9 @@ import com.taskttl.data.local.model.Task
* @param [showCompleted] 显示已完成
*/
data class TaskState(
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
val tasks: List = emptyList(),
val categories: List = emptyList(),
val editingTask: Task? = null,
@@ -25,10 +29,8 @@ data class TaskState(
val selectedCategory: Category? = null,
val isSearch: Boolean = false,
val searchQuery: String = "",
- val isLoading: Boolean = false,
- val error: String? = null,
val showCompleted: Boolean = false
-)
+): BaseUiState()
/**
* 任务意图
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CategoryViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CategoryViewModel.kt
index 8b0281a..bd0feae 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CategoryViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CategoryViewModel.kt
@@ -1,19 +1,13 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType
import com.taskttl.data.repository.CategoryRepository
import com.taskttl.data.state.CategoryEffect
import com.taskttl.data.state.CategoryIntent
import com.taskttl.data.state.CategoryState
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -37,20 +31,15 @@ import taskttl.composeapp.generated.resources.category_update_success
* @constructor 创建[CategoryViewModel]
* @param [categoryRepository] 类别存储库
*/
-class CategoryViewModel(private val categoryRepository: CategoryRepository) : ViewModel() {
-
- private val _state = MutableStateFlow(CategoryState())
- val state: StateFlow = _state.asStateFlow()
-
- private val _effects = MutableSharedFlow()
- val effects: SharedFlow = _effects.asSharedFlow()
+class CategoryViewModel(private val categoryRepository: CategoryRepository) :
+ BaseViewModel(CategoryState()) {
init {
- handleIntent(CategoryIntent.LoadCategories)
- handleIntent(CategoryIntent.LoadCategoryStatistics)
+ processIntent(CategoryIntent.LoadCategories)
+ processIntent(CategoryIntent.LoadCategoryStatistics)
}
- fun handleIntent(intent: CategoryIntent) {
+ public override fun handleIntent(intent: CategoryIntent) {
when (intent) {
is CategoryIntent.LoadCategories -> loadCategories()
is CategoryIntent.LoadCategoriesByType -> loadCategoriesByType(intent.type)
@@ -78,25 +67,25 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
*/
private fun loadCategories() {
viewModelScope.launch {
- _state.value = _state.value.copy(isLoading = true, error = null)
+ updateState { copy(isLoading = true, error = null) }
try {
categoryRepository.getAllCategories().collect { categories ->
val taskCategories = categories.filter { it.type == CategoryType.TASK }
val countdownCategories =
categories.filter { it.type == CategoryType.COUNTDOWN }
- _state.value = _state.value.copy(
- categories = categories,
- taskCategories = taskCategories,
- countdownCategories = countdownCategories,
- isLoading = false
- )
+ updateState {
+ copy(
+ categories = categories,
+ taskCategories = taskCategories,
+ countdownCategories = countdownCategories,
+ isLoading = false
+ )
+ }
}
} catch (e: Exception) {
- _state.value = _state.value.copy(
- isLoading = false,
- error = e.message ?: getString(Res.string.category_load_failed)
- )
+ val errStr = getString(Res.string.category_load_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -109,10 +98,10 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
val category = categoryRepository.getCategoryById(categoryId)
- _state.value = _state.value.copy(editingCategory = category)
+ updateState { copy(editingCategory = category) }
} catch (e: Exception) {
- _state.value =
- _state.value.copy(error = e.message ?: getString(Res.string.category_not_found))
+ val errStr = getString(Res.string.category_not_found)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -127,18 +116,17 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
categoryRepository.getCategoriesByType(type).collect { categories ->
when (type) {
CategoryType.TASK -> {
- _state.value = _state.value.copy(taskCategories = categories)
+ updateState { copy(taskCategories = categories) }
}
CategoryType.COUNTDOWN -> {
- _state.value = _state.value.copy(countdownCategories = categories)
+ updateState { copy(countdownCategories = categories) }
}
}
}
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_load_failed)
- )
+ val errStr = getString(Res.string.category_load_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -150,22 +138,21 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.getCategoryStatistics().collect { statistics ->
- _state.value = _state.value.copy(categoryStatistics = statistics)
+ updateState { copy(categoryStatistics = statistics) }
}
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_stat_failed)
- )
+ val errStr = getString(Res.string.category_stat_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
private fun selectCategory(category: Category?) {
- _state.value = _state.value.copy(selectedCategory = category)
+ updateState { copy(selectedCategory = category) }
}
private fun selectType(type: CategoryType) {
- _state.value = _state.value.copy(selectedType = type)
+ updateState { copy(selectedType = type) }
}
/**
@@ -176,12 +163,11 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.insertCategory(category)
- _effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_add_success)))
- _effects.emit(CategoryEffect.NavigateBack)
+ sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_add_success)))
+ sendEvent(CategoryEffect.NavigateBack)
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_add_failed)
- )
+ val errStr = getString(Res.string.category_add_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -190,13 +176,12 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.updateCategory(category)
- _effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_update_success)))
- _effects.emit(CategoryEffect.NavigateBack)
+ sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_update_success)))
+ sendEvent(CategoryEffect.NavigateBack)
hideEditDialog()
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_update_failed)
- )
+ val errStr = getString(Res.string.category_update_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -205,12 +190,11 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.deleteCategory(categoryId)
- _effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_delete_success)))
+ sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_delete_success)))
hideDeleteDialog()
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_delete_failed)
- )
+ val errStr = getString(Res.string.category_delete_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -219,11 +203,10 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.initializeDefaultCategories()
- _effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_init_success)))
+ sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_init_success)))
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_init_failed)
- )
+ val errStr = getString(Res.string.category_init_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -233,50 +216,37 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
try {
categoryRepository.updateCategoryCounts()
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.category_count_update_failed)
- )
+ val errStr = getString(Res.string.category_count_update_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
private fun showAddDialog() {
- _state.value = _state.value.copy(showAddDialog = true)
+ updateState { copy(showAddDialog = true) }
}
private fun hideAddDialog() {
- _state.value = _state.value.copy(showAddDialog = false)
+ updateState { copy(showAddDialog = false) }
}
private fun showEditDialog(category: Category) {
- _state.value = _state.value.copy(
- selectedCategory = category,
- showEditDialog = true
- )
+ updateState { copy(selectedCategory = category, showEditDialog = true) }
}
private fun hideEditDialog() {
- _state.value = _state.value.copy(
- selectedCategory = null,
- showEditDialog = false
- )
+ updateState { copy(selectedCategory = null, showEditDialog = false) }
}
private fun showDeleteDialog(category: Category) {
- _state.value = _state.value.copy(
- selectedCategory = category,
- showDeleteDialog = true
- )
+ updateState { copy(selectedCategory = category, showDeleteDialog = true) }
}
private fun hideDeleteDialog() {
- _state.value = _state.value.copy(
- selectedCategory = null,
- showDeleteDialog = false
- )
+ updateState { copy(selectedCategory = null, showDeleteDialog = false) }
}
private fun clearError() {
- _state.value = _state.value.copy(error = null)
+ updateState { copy(error = null) }
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CountdownViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CountdownViewModel.kt
index 71d6a67..ea81ed6 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CountdownViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CountdownViewModel.kt
@@ -1,7 +1,7 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType
import com.taskttl.data.local.model.Countdown
@@ -10,12 +10,6 @@ import com.taskttl.data.repository.CountdownRepository
import com.taskttl.data.state.CountdownEffect
import com.taskttl.data.state.CountdownIntent
import com.taskttl.data.state.CountdownState
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -37,20 +31,15 @@ import taskttl.composeapp.generated.resources.countdown_update_success
*/
class CountdownViewModel(
private val countdownRepository: CountdownRepository,
- private val categoryRepository: CategoryRepository
-) : ViewModel() {
+ private val categoryRepository: CategoryRepository,
+) : BaseViewModel(CountdownState()) {
- private val _state = MutableStateFlow(CountdownState())
- val state: StateFlow = _state.asStateFlow()
-
- private val _effects = MutableSharedFlow()
- val effects: SharedFlow = _effects.asSharedFlow()
init {
- handleIntent(CountdownIntent.LoadCountdowns)
+ processIntent(CountdownIntent.LoadCountdowns)
}
- fun handleIntent(intent: CountdownIntent) {
+ public override fun handleIntent(intent: CountdownIntent) {
when (intent) {
is CountdownIntent.LoadCountdowns -> loadCountdowns()
is CountdownIntent.GetCountdownById -> getCountdownById(intent.countdownId)
@@ -64,29 +53,27 @@ class CountdownViewModel(
private fun loadCountdowns() {
viewModelScope.launch {
- _state.value = _state.value.copy(isLoading = true, error = null)
+ updateState { copy(isLoading = true) }
try {
launch {
categoryRepository.getCategoriesByType(CategoryType.COUNTDOWN)
- .collect { categories ->
- _state.value = _state.value.copy(categories = categories)
- }
+ .collect { categories -> updateState { copy(categories = categories) } }
}
launch {
countdownRepository.getAllCountdowns().collect { countdowns ->
- _state.value = _state.value.copy(
- countdowns = countdowns,
- filteredCountdowns = filterCountdowns(countdowns),
- isLoading = false
- )
+ updateState {
+ copy(
+ countdowns = countdowns,
+ filteredCountdowns = filterCountdowns(countdowns)
+ )
+ }
}
}
} catch (e: Exception) {
- _state.value =
- _state.value.copy(
- isLoading = false,
- error = e.message ?: getString(Res.string.countdown_load_failed)
- )
+ val errStr = getString(Res.string.countdown_load_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
+ } finally {
+ updateState { copy(isLoading = false) }
}
}
}
@@ -96,11 +83,10 @@ class CountdownViewModel(
viewModelScope.launch {
try {
val countdown = countdownRepository.getCountdownById(countdownId)
- _state.value = _state.value.copy(editingCountdown = countdown)
+ updateState { copy(editingCountdown = countdown) }
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.countdown_query_failed)
- )
+ val errStr = getString(Res.string.countdown_query_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -109,12 +95,11 @@ class CountdownViewModel(
viewModelScope.launch {
try {
countdownRepository.insertCountdown(countdown)
- _effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_add_success)))
- _effects.emit(CountdownEffect.NavigateBack)
+ sendEvent(CountdownEffect.ShowMessage(getString(Res.string.countdown_add_success)))
+ sendEvent(CountdownEffect.NavigateBack)
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.countdown_add_failed)
- )
+ val errStr = getString(Res.string.countdown_add_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -123,11 +108,11 @@ class CountdownViewModel(
viewModelScope.launch {
try {
countdownRepository.updateCountdown(countdown)
- _effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_update_success)))
+ sendEvent(CountdownEffect.ShowMessage(getString(Res.string.countdown_update_success)))
+ sendEvent(CountdownEffect.NavigateBack)
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.countdown_update_failed)
- )
+ val errStr = getString(Res.string.countdown_update_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -136,26 +121,29 @@ class CountdownViewModel(
viewModelScope.launch {
try {
countdownRepository.deleteCountdown(countdownId)
- _effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_delete_success)))
+ sendEvent(CountdownEffect.ShowMessage(getString(Res.string.countdown_delete_success)))
} catch (e: Exception) {
- _state.value = _state.value.copy(error = e.message ?: getString(Res.string.countdown_delete_failed))
+ val errStr = getString(Res.string.countdown_delete_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
private fun filterByCategory(category: Category?) {
- _state.value = _state.value.copy(
- selectedCategory = category,
- filteredCountdowns = filterCountdowns(_state.value.countdowns)
- )
+ updateState {
+ copy(
+ selectedCategory = category,
+ filteredCountdowns = filterCountdowns(state.value.countdowns)
+ )
+ }
}
private fun clearError() {
- _state.value = _state.value.copy(error = null)
+ updateState { copy(error = null) }
}
private fun filterCountdowns(countdowns: List): List {
- val currentState = _state.value
+ val currentState = state.value
return countdowns.filter { countdown ->
currentState.selectedCategory?.let { countdown.category == it } ?: true
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt
index f016b5e..b355245 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt
@@ -1,19 +1,12 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.network.TaskTTLApi
import com.taskttl.data.network.domain.req.FeedbackReq
import com.taskttl.data.state.FeedbackEffect
import com.taskttl.data.state.FeedbackIntent
import com.taskttl.data.state.FeedbackState
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -26,16 +19,11 @@ import taskttl.composeapp.generated.resources.feedback_success
* @date 2025/10/12
* @constructor 创建[FeedbackViewModel]
*/
-class FeedbackViewModel() : ViewModel() {
-
- private val _state = MutableStateFlow(FeedbackState())
- val state: StateFlow = _state.asStateFlow()
-
- private val _effects = MutableSharedFlow()
- val effects: SharedFlow = _effects.asSharedFlow()
+class FeedbackViewModel() :
+ BaseViewModel(FeedbackState()) {
- fun handleIntent(intent: FeedbackIntent) {
+ public override fun handleIntent(intent: FeedbackIntent) {
when (intent) {
is FeedbackIntent.SubmitFeedback -> submitFeedback(intent.feedback)
is FeedbackIntent.ClearError -> clearError()
@@ -44,20 +32,17 @@ class FeedbackViewModel() : ViewModel() {
private fun submitFeedback(feedback: FeedbackReq) {
viewModelScope.launch {
- _state.value = _state.value.copy(isLoading = true, error = null)
+ if (state.value.isProcessing) return@launch
+ updateState { copy(isLoading = true, isProcessing = true) }
try {
- delay(10_000)
TaskTTLApi.postFeedback(feedback)
- _effects.emit(FeedbackEffect.ShowMessage(getString(Res.string.feedback_success)))
- _state.value = _state.value.copy(isLoading = false)
- _effects.emit(FeedbackEffect.NavigateBack)
+ sendEvent(FeedbackEffect.ShowMessage(getString(Res.string.feedback_success)))
+ sendEvent(FeedbackEffect.NavigateBack)
} catch (e: Exception) {
- _state.value = _state.value.copy(isLoading = false, error = e.message)
- _effects.emit(
- FeedbackEffect.ShowMessage(
- e.message ?: getString(Res.string.feedback_error)
- )
- )
+ val errStr = getString(Res.string.feedback_error)
+ sendEvent(FeedbackEffect.ShowMessage(e.message ?: errStr))
+ } finally {
+ updateState { copy(isLoading = false, isProcessing = false) }
}
}
@@ -67,7 +52,7 @@ class FeedbackViewModel() : ViewModel() {
* 清除错误
*/
private fun clearError() {
- _state.value = _state.value.copy(error = null)
+ updateState { copy(error = null) }
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/OnboardingViewModel.kt
index f34d333..959c899 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/OnboardingViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/OnboardingViewModel.kt
@@ -1,13 +1,12 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.repository.CategoryRepository
import com.taskttl.data.repository.OnboardingRepository
-import com.taskttl.data.state.OnboardingEvent
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.receiveAsFlow
+import com.taskttl.data.state.OnboardingEffect
+import com.taskttl.data.state.OnboardingIntent
+import com.taskttl.data.state.OnboardingState
import kotlinx.coroutines.launch
/**
@@ -20,30 +19,37 @@ import kotlinx.coroutines.launch
*/
class OnboardingViewModel(
private val onboardingRepository: OnboardingRepository,
- private val categoryRepository: CategoryRepository
-) : ViewModel() {
+ private val categoryRepository: CategoryRepository,
+) : BaseViewModel(initialState = OnboardingState()) {
- private val _events = Channel()
- val events: Flow = _events.receiveAsFlow()
+ override fun handleIntent(intent: OnboardingIntent) {
+ when (intent) {
+ is OnboardingIntent.NextPage -> nextPage()
+ is OnboardingIntent.MarkOnboardingCompleted -> markOnboardingCompleted()
+ }
+ }
/**
- * 发送事件 - 提供统一的事件发送机制
- * @param event 事件
+ * 下一页
*/
- fun sendEvent(event: OnboardingEvent) {
- viewModelScope.launch {
- _events.trySend(event)
- }
+ private fun nextPage() {
+ sendEvent(OnboardingEffect.NextPage)
}
/**
* 标记引导完成
*/
- fun markOnboardingCompleted() {
+ private fun markOnboardingCompleted() {
viewModelScope.launch {
- categoryRepository.initializeDefaultCategories()
- onboardingRepository.markLaunched()
- _events.trySend(OnboardingEvent.NavMain)
+ try {
+ if (state.value.isProcessing) return@launch
+ updateState { copy(isLoading = false, isProcessing = true) }
+ categoryRepository.initializeDefaultCategories()
+ onboardingRepository.markLaunched()
+ sendEvent(OnboardingEffect.NavMain)
+ } finally {
+ updateState { copy(isLoading = false, isProcessing = false) }
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SettingsViewModel.kt
index 669989f..a42e0f1 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SettingsViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SettingsViewModel.kt
@@ -1,17 +1,11 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.utils.ExternalAppLauncher
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.state.SettingsEffect
import com.taskttl.data.state.SettingsIntent
import com.taskttl.data.state.SettingsState
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
@@ -20,16 +14,11 @@ import kotlinx.coroutines.launch
* @date 2025/10/12
* @constructor 创建[SettingsViewModel]
*/
-class SettingsViewModel() : ViewModel() {
-
- private val _state = MutableStateFlow(SettingsState())
- val state: StateFlow = _state.asStateFlow()
-
- private val _effects = MutableSharedFlow()
- val effects: SharedFlow = _effects.asSharedFlow()
+class SettingsViewModel() :
+ BaseViewModel(SettingsState()) {
- fun handleIntent(intent: SettingsIntent) {
+ public override fun handleIntent(intent: SettingsIntent) {
when (intent) {
is SettingsIntent.OpenAppRating -> openAppRating()
is SettingsIntent.OpenUrl -> openUrl(intent.url)
@@ -42,7 +31,7 @@ class SettingsViewModel() : ViewModel() {
}
}
- private fun openUrl(url:String) {
+ private fun openUrl(url: String) {
viewModelScope.launch {
ExternalAppLauncher.openUrl(url)
}
@@ -52,7 +41,7 @@ class SettingsViewModel() : ViewModel() {
* 清除错误
*/
private fun clearError() {
- _state.value = _state.value.copy(error = null)
+ updateState { copy(error = null) }
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SplashViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SplashViewModel.kt
index 45e0923..47b237b 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SplashViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SplashViewModel.kt
@@ -1,17 +1,15 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.domain.constant.PointEvent
import com.taskttl.core.utils.DeviceUtils
-import com.taskttl.core.utils.LogUtils
import com.taskttl.core.utils.StorageUtils
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.network.TaskTTLApi
import com.taskttl.data.repository.OnboardingRepository
+import com.taskttl.data.state.SplashEffect
+import com.taskttl.data.state.SplashIntent
import com.taskttl.data.state.SplashState
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
@@ -23,12 +21,19 @@ import kotlinx.coroutines.launch
*/
class SplashViewModel(
private val onboardingRepository: OnboardingRepository,
-) : ViewModel() {
-
- private val _uiState = MutableStateFlow(SplashState.Loading)
- val uiState: StateFlow = _uiState.asStateFlow()
+) : BaseViewModel(SplashState()) {
init {
+ processIntent(SplashIntent.LoadApp)
+ }
+
+ override fun handleIntent(intent: SplashIntent) {
+ when (intent) {
+ is SplashIntent.LoadApp -> loadApp()
+ }
+ }
+
+ private fun loadApp() {
viewModelScope.launch {
DeviceUtils.getUniqueId()
@@ -40,8 +45,11 @@ class SplashViewModel(
TaskTTLApi.postPoint(PointEvent.AppLaunch)
}
val hasLaunched = onboardingRepository.isLaunchedBefore()
- _uiState.value =
- if (hasLaunched) SplashState.NavigateToOnboarding else SplashState.NavigateToMain
+ if (hasLaunched) {
+ sendEvent(SplashEffect.NavigateToOnboarding)
+ } else {
+ sendEvent(SplashEffect.NavigateToMain)
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/TaskViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/TaskViewModel.kt
index adce2eb..2080717 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/TaskViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/TaskViewModel.kt
@@ -1,8 +1,8 @@
package com.taskttl.data.viewmodel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.utils.LogUtils
+import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType
import com.taskttl.data.local.model.Task
@@ -11,12 +11,6 @@ import com.taskttl.data.repository.TaskRepository
import com.taskttl.data.state.TaskEffect
import com.taskttl.data.state.TaskIntent
import com.taskttl.data.state.TaskState
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -40,20 +34,15 @@ import taskttl.composeapp.generated.resources.task_update_success
*/
class TaskViewModel(
private val taskRepository: TaskRepository,
- private val categoryRepository: CategoryRepository
-) : ViewModel() {
+ private val categoryRepository: CategoryRepository,
+) : BaseViewModel(TaskState()) {
- private val _state = MutableStateFlow(TaskState())
- val state: StateFlow = _state.asStateFlow()
-
- private val _effects = MutableSharedFlow()
- val effects: SharedFlow = _effects.asSharedFlow()
init {
- handleIntent(TaskIntent.LoadTasks)
+ processIntent(TaskIntent.LoadTasks)
}
- fun handleIntent(intent: TaskIntent) {
+ public override fun handleIntent(intent: TaskIntent) {
when (intent) {
is TaskIntent.LoadTasks -> loadTasks()
is TaskIntent.GetTaskById -> getTaskById(intent.taskId)
@@ -76,7 +65,7 @@ class TaskViewModel(
*/
private fun navigateBack() {
viewModelScope.launch {
- _effects.emit(TaskEffect.NavigateBack)
+ sendEvent(TaskEffect.NavigateBack)
}
}
@@ -85,7 +74,7 @@ class TaskViewModel(
*/
private fun navigateToEditTask() {
viewModelScope.launch {
- _effects.emit(TaskEffect.NavigateToEditTask)
+ sendEvent(TaskEffect.NavigateToEditTask)
}
}
@@ -94,31 +83,30 @@ class TaskViewModel(
*/
private fun loadTasks() {
viewModelScope.launch {
- _state.value = _state.value.copy(isLoading = true, error = null)
+ updateState { copy(isLoading = true, error = null) }
try {
launch {
categoryRepository.getCategoriesByType(CategoryType.TASK)
.collect { categories ->
LogUtils.e("DevTTL", categories.toString())
- _state.value = _state.value.copy(categories = categories)
+ updateState { copy(categories = categories) }
}
}
launch {
taskRepository.getAllTasks().collect { tasks ->
- _state.value = _state.value.copy(
- tasks = tasks,
- filteredTasks = filterTasks(tasks),
- isLoading = false
- )
+ updateState {
+ copy(
+ tasks = tasks,
+ filteredTasks = filterTasks(tasks),
+ isLoading = false
+ )
+ }
}
}
} catch (e: Exception) {
LogUtils.e("DevTTL", e.message.toString())
- _state.value =
- _state.value.copy(
- isLoading = false,
- error = e.message ?: getString(Res.string.task_load_failed)
- )
+ val errStr = getString(Res.string.task_load_failed)
+ updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -131,10 +119,10 @@ class TaskViewModel(
viewModelScope.launch {
try {
val task = taskRepository.getTaskById(taskId)
- _state.value = _state.value.copy(editingTask = task)
+ updateState { copy(editingTask = task) }
} catch (e: Exception) {
- _state.value =
- _state.value.copy(error = e.message ?: getString(Res.string.task_query_failed))
+ val errStr = getString(Res.string.task_query_failed)
+ updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -147,11 +135,11 @@ class TaskViewModel(
viewModelScope.launch {
try {
taskRepository.insertTask(task)
- _effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_add_success)))
- _effects.emit(TaskEffect.NavigateBack)
+ sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_add_success)))
+ sendEvent(TaskEffect.NavigateBack)
} catch (e: Exception) {
- _state.value =
- _state.value.copy(error = e.message ?: getString(Res.string.task_add_failed))
+ val errStr = getString(Res.string.task_add_failed)
+ updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -164,11 +152,11 @@ class TaskViewModel(
viewModelScope.launch {
try {
taskRepository.updateTask(task)
- _effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_update_success)))
- _effects.emit(TaskEffect.NavigateBack)
+ sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_update_success)))
+ sendEvent(TaskEffect.NavigateBack)
} catch (e: Exception) {
- _state.value =
- _state.value.copy(error = e.message ?: getString(Res.string.task_update_failed))
+ val errStr = getString(Res.string.task_update_failed)
+ updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -181,10 +169,10 @@ class TaskViewModel(
viewModelScope.launch {
try {
taskRepository.deleteTask(taskId)
- _effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
+ sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
} catch (e: Exception) {
- _state.value =
- _state.value.copy(error = e.message ?: getString(Res.string.task_delete_failed))
+ val errStr = getString(Res.string.task_delete_failed)
+ updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -196,16 +184,15 @@ class TaskViewModel(
private fun toggleTaskCompletion(taskId: String) {
viewModelScope.launch {
try {
- val task = _state.value.tasks.find { it.id == taskId }
+ val task = state.value.tasks.find { it.id == taskId }
task?.let {
val updatedTask = it.copy(isCompleted = !it.isCompleted)
taskRepository.updateTask(updatedTask)
- _effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_status_update_success)))
+ sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_status_update_success)))
}
} catch (e: Exception) {
- _state.value = _state.value.copy(
- error = e.message ?: getString(Res.string.task_status_update_failed)
- )
+ val errStr = getString(Res.string.task_status_update_failed)
+ updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -215,8 +202,8 @@ class TaskViewModel(
* @param [category] 类别
*/
private fun filterByCategory(category: Category?) {
- _state.value = _state.value.copy(selectedCategory = category)
- _state.value = _state.value.copy(filteredTasks = filterTasks(_state.value.tasks))
+ updateState { copy(selectedCategory = category) }
+ updateState { copy(filteredTasks = filterTasks(state.value.tasks)) }
}
/**
@@ -224,8 +211,8 @@ class TaskViewModel(
* @param [query] 怎么翻译
*/
private fun searchTasks(query: String) {
- _state.value = _state.value.copy(searchQuery = query)
- _state.value = _state.value.copy(filteredTasks = filterTasks(_state.value.tasks))
+ updateState { copy(searchQuery = query) }
+ updateState { copy(filteredTasks = filterTasks(state.value.tasks)) }
}
/**
@@ -233,22 +220,22 @@ class TaskViewModel(
* @param [show] 显示
*/
private fun toggleShowCompleted(show: Boolean) {
- _state.value = _state.value.copy(showCompleted = show)
- _state.value = _state.value.copy(filteredTasks = filterTasks(_state.value.tasks))
+ updateState { copy(showCompleted = show) }
+ updateState { copy(filteredTasks = filterTasks(state.value.tasks)) }
}
/**
* 搜索视图
*/
private fun searchView() {
- _state.value = _state.value.copy(isSearch = !_state.value.isSearch)
+ updateState { copy(isSearch = !state.value.isSearch) }
}
/**
* 清除错误
*/
private fun clearError() {
- _state.value = _state.value.copy(error = null)
+ updateState { copy(error = null) }
}
/**
@@ -257,7 +244,7 @@ class TaskViewModel(
* @return [List]
*/
private fun filterTasks(tasks: List): List {
- val currentState = _state.value
+ val currentState = state.value
return tasks.filter { task ->
val categoryMatch = currentState.selectedCategory?.let { task.category == it } ?: true
@@ -274,4 +261,4 @@ class TaskViewModel(
categoryMatch && searchMatch && completionMatch
}
}
-}
\ No newline at end of file
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/onboarding/OnboardingScreen.kt
index 2abf469..cd2c276 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/onboarding/OnboardingScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/onboarding/OnboardingScreen.kt
@@ -23,6 +23,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -32,7 +34,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.taskttl.core.routes.Routes
import com.taskttl.data.local.model.OnboardingPage
-import com.taskttl.data.state.OnboardingEvent
+import com.taskttl.data.state.OnboardingEffect
+import com.taskttl.data.state.OnboardingIntent
import com.taskttl.data.viewmodel.OnboardingViewModel
import kotlinx.coroutines.flow.collectLatest
import org.jetbrains.compose.resources.stringResource
@@ -52,21 +55,22 @@ import taskttl.composeapp.generated.resources.skip_text
@Composable
fun OnboardingScreen(
navigatorToRoute: (Routes) -> Unit,
- viewModel: OnboardingViewModel = koinViewModel()
+ viewModel: OnboardingViewModel = koinViewModel(),
) {
- val onboardingPages = OnboardingPage.entries
+ val state by viewModel.state.collectAsState()
+ val onboardingPages = state.pages
val pagerState =
rememberPagerState(0, initialPageOffsetFraction = 0f, pageCount = { onboardingPages.size })
LaunchedEffect(Unit) {
- viewModel.events.collectLatest { event ->
+ viewModel.effects.collectLatest { event ->
when (event) {
- is OnboardingEvent.NextPage -> {
+ is OnboardingEffect.NextPage -> {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
- is OnboardingEvent.NavMain -> {
+ is OnboardingEffect.NavMain -> {
navigatorToRoute(Routes.Main)
}
}
@@ -76,7 +80,7 @@ fun OnboardingScreen(
Box(modifier = Modifier.fillMaxSize()) {
// 右上角跳过
TextButton(
- onClick = { viewModel.markOnboardingCompleted() },
+ onClick = { viewModel.processIntent(OnboardingIntent.MarkOnboardingCompleted) },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 32.dp, end = 16.dp)
@@ -122,7 +126,7 @@ fun OnboardingScreen(
Button(
onClick = {
if (pagerState.currentPage < onboardingPages.lastIndex) {
- viewModel.sendEvent(OnboardingEvent.NextPage)
+ viewModel.processIntent(OnboardingIntent.NextPage)
}
},
modifier = Modifier.fillMaxWidth(),
@@ -141,7 +145,7 @@ fun OnboardingScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
- onClick = { viewModel.markOnboardingCompleted() },
+ onClick = { viewModel.processIntent(OnboardingIntent.MarkOnboardingCompleted) },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF667EEA)),
shape = MaterialTheme.shapes.medium
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/splash/SplashScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/splash/SplashScreen.kt
index 9b5eadb..6235400 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/splash/SplashScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/splash/SplashScreen.kt
@@ -21,7 +21,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -33,7 +32,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.taskttl.core.routes.Routes
-import com.taskttl.data.state.SplashState
+import com.taskttl.data.state.SplashEffect
import com.taskttl.data.viewmodel.SplashViewModel
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -51,15 +50,20 @@ import taskttl.composeapp.generated.resources.app_name_remark
@Composable
fun SplashScreen(
navigatorToRoute: (Routes) -> Unit,
- viewModel: SplashViewModel = koinViewModel()
+ viewModel: SplashViewModel = koinViewModel(),
) {
- val state by viewModel.uiState.collectAsState()
- LaunchedEffect(state) {
- when (state) {
- SplashState.NavigateToOnboarding -> navigatorToRoute(Routes.Onboarding)
- SplashState.NavigateToMain -> navigatorToRoute(Routes.Main)
- else -> {}
+ LaunchedEffect(Unit) {
+ viewModel.effects.collect { effect ->
+ when (effect) {
+ is SplashEffect.NavigateToOnboarding -> {
+ navigatorToRoute(Routes.Onboarding)
+ }
+
+ is SplashEffect.NavigateToMain -> {
+ navigatorToRoute(Routes.Main)
+ }
+ }
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt
index 894f70a..9367808 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt
@@ -71,14 +71,14 @@ import taskttl.composeapp.generated.resources.total_tasks
fun StatisticsScreen(
navController: NavHostController,
taskViewModel: TaskViewModel = koinViewModel(),
- countdownViewModel: CountdownViewModel = koinViewModel()
+ countdownViewModel: CountdownViewModel = koinViewModel(),
) {
val taskState by taskViewModel.state.collectAsState()
val countdownState by countdownViewModel.state.collectAsState()
LaunchedEffect(Unit) {
- taskViewModel.handleIntent(TaskIntent.LoadTasks)
+ taskViewModel.processIntent(TaskIntent.LoadTasks)
countdownViewModel.handleIntent(CountdownIntent.LoadCountdowns)
}
@@ -220,7 +220,7 @@ private fun StatisticCard(
value: String,
icon: ImageVector,
color: Color,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
@@ -264,7 +264,7 @@ private fun CategoryStatisticItem(
category: Category,
totalCount: Int,
completedCount: Int,
- typeRes: StringResource
+ typeRes: StringResource,
) {
if (totalCount == 0) return
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskScreen.kt
index 34c4f88..bb17c4f 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskScreen.kt
@@ -81,7 +81,7 @@ import taskttl.composeapp.generated.resources.title_task
@Preview
fun TaskScreen(
navController: NavHostController,
- viewModel: TaskViewModel = koinViewModel()
+ viewModel: TaskViewModel = koinViewModel(),
) {
val state by viewModel.state.collectAsState()
@@ -283,7 +283,7 @@ fun TaskCardItem(
onClick: () -> Unit,
onToggleComplete: () -> Unit,
onDeleteTask: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
ActionButtonListItem(
modifier = Modifier
@@ -339,13 +339,29 @@ fun TaskCardItem(
if (task.description.isNotBlank()) {
Spacer(modifier = Modifier.height(4.dp))
- Text(
- text = task.description,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
+ Row() {
+ Text(
+ text = task.description,
+ style = MaterialTheme.typography.bodySmall,
+ textDecoration = if (task.isCompleted) TextDecoration.LineThrough else null,
+ color = if (task.isCompleted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ // 结束时间
+ task.dueDate?.let {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = task.dueDate.toString(),
+ style = MaterialTheme.typography.bodySmall,
+ textDecoration = if (task.isCompleted) TextDecoration.LineThrough else null,
+ color = if (task.isCompleted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
}
}