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