更新
This commit is contained in:
@@ -14,6 +14,5 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
fun App() {
|
||||
AppTheme {
|
||||
AppNav()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.taskttl.core.config
|
||||
|
||||
import com.taskttl.core.domain.BaseReq
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
/**
|
||||
* T: 请求类
|
||||
* B: BaseReq 类型
|
||||
*/
|
||||
class FlattenBaseReqSerializer<T : Any, B : BaseReq>(
|
||||
private val valueSerializer: KSerializer<T>,
|
||||
private val baseSelector: (T) -> B
|
||||
) : KSerializer<T> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("FlattenBaseReq") {
|
||||
for (i in 0 until valueSerializer.descriptor.elementsCount) {
|
||||
val name = valueSerializer.descriptor.getElementName(i)
|
||||
element(name, PrimitiveSerialDescriptor(name, PrimitiveKind.STRING), isOptional = true)
|
||||
}
|
||||
// baseReq 的字段也展开
|
||||
element("appName", PrimitiveSerialDescriptor("appName", PrimitiveKind.STRING))
|
||||
element("versionCode", PrimitiveSerialDescriptor("versionCode", PrimitiveKind.INT))
|
||||
element("appId", PrimitiveSerialDescriptor("appId", PrimitiveKind.INT))
|
||||
element("uniqueId", PrimitiveSerialDescriptor("uniqueId", PrimitiveKind.STRING))
|
||||
element("deviceInfo", PrimitiveSerialDescriptor("deviceInfo", PrimitiveKind.STRING))
|
||||
element("deviceVersion", PrimitiveSerialDescriptor("deviceVersion", PrimitiveKind.STRING))
|
||||
element("language", PrimitiveSerialDescriptor("language", PrimitiveKind.STRING))
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
val jsonEncoder = encoder as? JsonEncoder ?: error("Only JSON supported")
|
||||
val base = baseSelector(value)
|
||||
val jsonObj = jsonEncoder.json.encodeToJsonElement(valueSerializer, value).jsonObject.toMutableMap()
|
||||
|
||||
// 将 baseReq 字段平级展开
|
||||
jsonObj["appName"] = JsonPrimitive(base.appName)
|
||||
jsonObj["versionCode"] = JsonPrimitive(base.versionCode)
|
||||
jsonObj["appId"] = JsonPrimitive(base.appId)
|
||||
jsonObj["uniqueId"] = JsonPrimitive(base.uniqueId)
|
||||
jsonObj["deviceInfo"] = JsonPrimitive(base.deviceInfo)
|
||||
jsonObj["deviceVersion"] = JsonPrimitive(base.deviceVersion)
|
||||
jsonObj["language"] = JsonPrimitive(base.language)
|
||||
|
||||
jsonEncoder.encodeJsonElement(JsonObject(jsonObj))
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
val jsonDecoder = decoder as? JsonDecoder ?: error("Only JSON supported")
|
||||
val jsonObj = jsonDecoder.decodeJsonElement().jsonObject.toMutableMap()
|
||||
|
||||
// 解析 BaseReq
|
||||
val base = BaseReq(
|
||||
appName = jsonObj["appName"]!!.jsonPrimitive.content,
|
||||
versionCode = jsonObj["versionCode"]!!.jsonPrimitive.int,
|
||||
appId = jsonObj["appId"]!!.jsonPrimitive.int,
|
||||
uniqueId = jsonObj["uniqueId"]!!.jsonPrimitive.content,
|
||||
deviceInfo = jsonObj["deviceInfo"]!!.jsonPrimitive.content,
|
||||
deviceVersion = jsonObj["deviceVersion"]!!.jsonPrimitive.content,
|
||||
language = jsonObj["language"]!!.jsonPrimitive.content
|
||||
)
|
||||
|
||||
// 删除 baseReq 字段
|
||||
jsonObj.remove("appName")
|
||||
jsonObj.remove("versionCode")
|
||||
jsonObj.remove("appId")
|
||||
jsonObj.remove("uniqueId")
|
||||
jsonObj.remove("deviceInfo")
|
||||
jsonObj.remove("deviceVersion")
|
||||
jsonObj.remove("language")
|
||||
|
||||
// 反序列化剩下的业务字段
|
||||
val t = jsonDecoder.json.decodeFromJsonElement(valueSerializer, JsonObject(jsonObj))
|
||||
|
||||
// 将 base 通过构造器或 copy 注入业务对象
|
||||
if (t is BaseReqHolder) t.baseReq = base
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
/** 业务对象实现接口,持有 BaseReq */
|
||||
interface BaseReqHolder {
|
||||
var baseReq: BaseReq
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.taskttl.core.domain
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* API统一响应格式
|
||||
* @author DevTTL
|
||||
* @date 2025/03/10
|
||||
*/
|
||||
@Serializable
|
||||
data class ApiResponse<T>(val code: Int, val msg: String, val data: T)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.taskttl.core.domain
|
||||
|
||||
import com.taskttl.core.config.FlattenBaseReqSerializer
|
||||
import com.taskttl.core.utils.JsonUtils
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
/**
|
||||
* 基础请求
|
||||
* @author admin
|
||||
* @date 2025/04/09
|
||||
* @constructor 创建[BaseReq]
|
||||
* @param [appName] 应用程序名称
|
||||
* @param [versionCode] 版本代码
|
||||
* @param [appId] 应用ID
|
||||
* @param [uniqueId] 唯一id
|
||||
* @param [deviceInfo] 设备信息
|
||||
* @param [deviceVersion] 设备版本
|
||||
* @param [language] 语言
|
||||
*/
|
||||
@Serializable
|
||||
open class BaseReq(
|
||||
open val appName: String,
|
||||
open val versionCode: Int,
|
||||
open val appId: Int,
|
||||
open val uniqueId: String,
|
||||
open val deviceInfo: String,
|
||||
open val deviceVersion: String,
|
||||
open val language: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
open class BaseReqWith(
|
||||
@Transient var baseReq: BaseReq = defaultBaseReq(),
|
||||
)
|
||||
|
||||
// 通用扩展函数
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
fun <T : BaseReqWith> T.toJson(): String {
|
||||
val serializer: KSerializer<T> = FlattenBaseReqSerializer(
|
||||
valueSerializer = this::class.serializer() as KSerializer<T>,
|
||||
baseSelector = { it.baseReq }
|
||||
)
|
||||
return JsonUtils.default.encodeToString(serializer, this)
|
||||
}
|
||||
|
||||
fun defaultBaseReq(): BaseReq = BaseReq(
|
||||
appName = "",
|
||||
versionCode = 0,
|
||||
appId = 0,
|
||||
uniqueId = "",
|
||||
deviceInfo = "",
|
||||
deviceVersion = "",
|
||||
language = ""
|
||||
)
|
||||
@@ -14,7 +14,7 @@ import taskttl.composeapp.generated.resources.feedback_suggestion
|
||||
* @param [titleRes] 标题res
|
||||
*/
|
||||
@Serializable
|
||||
enum class FeedbackType(val titleRes: StringResource) {
|
||||
ISSUE(Res.string.feedback_issue),
|
||||
SUGGESTION(Res.string.feedback_suggestion)
|
||||
enum class FeedbackType(var code: String,val titleRes: StringResource) {
|
||||
ISSUE("1",Res.string.feedback_issue),
|
||||
SUGGESTION("2",Res.string.feedback_suggestion)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.taskttl.core.domain.constant
|
||||
|
||||
/**
|
||||
* 埋点事件
|
||||
* @author DevTTL
|
||||
* @date 2025/09/04
|
||||
* @constructor 创建[PointEvent]
|
||||
* @param [eventName] 事件名称
|
||||
* @param [eventCode] 事件代码
|
||||
*/
|
||||
enum class PointEvent(val eventName: String, val eventCode: Int) {
|
||||
// 启动类事件
|
||||
FirstAppLaunch("first_app_launch", 1001),
|
||||
AppLaunch("app_launch", 1002),
|
||||
AppExit("app_exit", 1003),
|
||||
|
||||
// 页面浏览类
|
||||
PageViewHome("page_view_home", 2001),
|
||||
PageViewDetail("page_view_detail", 2002),
|
||||
PageViewProfile("page_view_profile", 2003),
|
||||
|
||||
// 按钮点击类
|
||||
ClickAdd("click_add", 3001),
|
||||
ClickBack("click_back", 3002),
|
||||
ClickShare("click_share", 3003),
|
||||
ClickLogin("click_login", 3004),
|
||||
|
||||
// 弹窗展示类
|
||||
DialogShownUpdate("dialog_shown_update", 4001),
|
||||
DialogShownPermission("dialog_shown_permission", 4002),
|
||||
|
||||
// 分享类
|
||||
ShareSuccess("share_success", 5001),
|
||||
ShareCancel("share_cancel", 5002),
|
||||
|
||||
// 登录注册类
|
||||
LoginSuccess("login_success", 6001),
|
||||
RegisterSuccess("register_success", 6002),
|
||||
|
||||
// 广告类
|
||||
AdShownBanner("ad_shown_banner", 9001),
|
||||
AdClickReward("ad_click_reward", 9002);
|
||||
|
||||
fun toMap(): Map<String, Any> = mapOf(
|
||||
"eventName" to eventName,
|
||||
"eventCode" to eventCode,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.taskttl.core.network
|
||||
|
||||
/**
|
||||
* api配置
|
||||
* @author DevTTL
|
||||
* @date 2025/10/11
|
||||
*/
|
||||
object ApiConfig {
|
||||
/** 基本地址 */
|
||||
const val BASE_URL = "http://10.0.0.5:8888/api/v1"
|
||||
// const val BASE_URL = "https://api.tikttl.com/api/v1"
|
||||
|
||||
/** 反馈地址 */
|
||||
const val FEEDBACK_URL = "$BASE_URL/feedback"
|
||||
|
||||
/** 埋点地址 */
|
||||
const val POINT_URL = "$BASE_URL/point"
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.taskttl.core.network
|
||||
|
||||
import com.taskttl.core.domain.ApiResponse
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.HttpRequestRetry
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.userAgent
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* ktor客户端
|
||||
* @author DevTTL
|
||||
* @date 2025/03/08
|
||||
*/
|
||||
object KtorClient {
|
||||
// 超时配置(毫秒)
|
||||
private const val CONNECT_TIMEOUT = 15000L
|
||||
private const val REQUEST_TIMEOUT = 30000L
|
||||
private const val SOCKET_TIMEOUT = 60000L
|
||||
|
||||
// 重试配置
|
||||
private const val MAX_RETRIES = 3
|
||||
private const val RETRY_DELAY = 1000L
|
||||
|
||||
val httpClient = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true // JSON多了字段也不报错
|
||||
encodeDefaults = false
|
||||
})
|
||||
}
|
||||
|
||||
// 日志插件
|
||||
install(Logging) {
|
||||
logger = defaultLogger
|
||||
level = LogLevel.ALL // 记录请求头、请求体、响应头、响应体
|
||||
}
|
||||
|
||||
// 超时配置
|
||||
install(HttpTimeout) {
|
||||
connectTimeoutMillis = CONNECT_TIMEOUT
|
||||
requestTimeoutMillis = REQUEST_TIMEOUT
|
||||
socketTimeoutMillis = SOCKET_TIMEOUT
|
||||
}
|
||||
// 重试配置
|
||||
install(HttpRequestRetry) {
|
||||
retryOnServerErrors(maxRetries = MAX_RETRIES)
|
||||
retryOnException(maxRetries = MAX_RETRIES)
|
||||
exponentialDelay()
|
||||
|
||||
// 自定义重试条件
|
||||
modifyRequest { request ->
|
||||
request.headers.append("X-Retry-Count", retryCount.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post Json
|
||||
* @param [url] 请求链接
|
||||
* @param [body] 请求内容
|
||||
* @return [String]
|
||||
*/
|
||||
suspend inline fun <reified T> postJson(url: String, body: String): T {
|
||||
try {
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
userAgent("TaskTTL")
|
||||
setBody(body)
|
||||
}
|
||||
return handleResponse(response)
|
||||
} catch (e: Exception) {
|
||||
throw Exception(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理API响应
|
||||
* @param [response] HTTP响应
|
||||
* @return [T] 响应数据
|
||||
* @throws Exception 当响应码不为200时抛出异常
|
||||
*/
|
||||
suspend inline fun <reified T> handleResponse(response: HttpResponse): T {
|
||||
// 使用统一响应处理
|
||||
val apiResponse = response.body<ApiResponse<T>>()
|
||||
if (apiResponse.code != 200) {
|
||||
throw Exception(apiResponse.msg)
|
||||
}
|
||||
return apiResponse.data
|
||||
}
|
||||
}
|
||||
|
||||
expect val defaultLogger: Logger
|
||||
@@ -176,10 +176,7 @@ fun MainNav() {
|
||||
}
|
||||
// 反馈页面
|
||||
composable<Main.Settings.Feedback> {
|
||||
FeedbackScreen(
|
||||
onNavigateBack = { mainNavController.popBackStack() },
|
||||
onSubmit = {}
|
||||
)
|
||||
FeedbackScreen(onNavigateBack = { mainNavController.popBackStack() })
|
||||
}
|
||||
// 隐私
|
||||
composable<Main.Settings.Privacy> {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.taskttl.core.ui
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.confirm
|
||||
import taskttl.composeapp.generated.resources.error_title
|
||||
|
||||
/**
|
||||
* 错误对话框
|
||||
* @param [errorMessage] 错误信息
|
||||
* @param [onDismiss] 解雇
|
||||
*/
|
||||
@Composable
|
||||
fun ErrorDialog(errorMessage: String?, onDismiss: () -> Unit) {
|
||||
if (errorMessage != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(Res.string.error_title)) },
|
||||
text = { Text(errorMessage) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.confirm))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.taskttl.core.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun LoadingScreen() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x88000000))
|
||||
// 拦截所有点击事件,防止点击到底层
|
||||
.clickable(
|
||||
indication = null, // 无点击动画
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.taskttl.core.utils
|
||||
|
||||
import com.taskttl.core.domain.BaseReq
|
||||
|
||||
/**
|
||||
* 设备工具
|
||||
* @author DevTTL
|
||||
* @date 2025/03/12
|
||||
* @constructor 创建[DeviceUtils]
|
||||
*/
|
||||
expect object DeviceUtils {
|
||||
|
||||
/**
|
||||
* 获取唯一id
|
||||
* @return [String]
|
||||
*/
|
||||
suspend fun getUniqueId(): String
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
* @return [BaseReq]
|
||||
*/
|
||||
suspend fun getDeviceInfo(): BaseReq
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.taskttl.core.utils
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* json-utils
|
||||
* @author DevTTL
|
||||
* @date 2025/10/11
|
||||
*/
|
||||
object JsonUtils {
|
||||
val default: Json = Json {
|
||||
prettyPrint = true
|
||||
encodeDefaults = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.taskttl.core.utils
|
||||
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
|
||||
enum class LogLevel { DEBUG, INFO, WARN, ERROR }
|
||||
|
||||
/**
|
||||
* 日志
|
||||
* 日志工具类
|
||||
* @author admin
|
||||
* @date 2025/10/03
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package com.taskttl.core.utils
|
||||
|
||||
/**
|
||||
* 吐司工具类
|
||||
* @author DevTTL
|
||||
* @date 2025/10/11
|
||||
*/
|
||||
expect object ToastUtils {
|
||||
fun show(message: String)
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.taskttl.data.di
|
||||
|
||||
import com.taskttl.data.repository.impl.OnboardingRepositoryImpl
|
||||
import com.taskttl.data.repository.SettingsRepository
|
||||
import com.taskttl.data.repository.impl.SettingsRepositoryImpl
|
||||
import com.taskttl.data.repository.OnboardingRepository
|
||||
import com.taskttl.data.viewmodel.OnboardingViewModel
|
||||
import com.taskttl.data.viewmodel.SplashViewModel
|
||||
import com.taskttl.data.repository.SettingsRepository
|
||||
import com.taskttl.data.repository.impl.OnboardingRepositoryImpl
|
||||
import com.taskttl.data.repository.impl.SettingsRepositoryImpl
|
||||
import com.taskttl.data.viewmodel.CategoryViewModel
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
import com.taskttl.data.viewmodel.FeedbackViewModel
|
||||
import com.taskttl.data.viewmodel.OnboardingViewModel
|
||||
import com.taskttl.data.viewmodel.SplashViewModel
|
||||
import com.taskttl.data.viewmodel.TaskViewModel
|
||||
import org.koin.core.KoinApplication
|
||||
import org.koin.core.context.startKoin
|
||||
@@ -43,4 +44,5 @@ val viewModelModule = module {
|
||||
viewModelOf(::TaskViewModel)
|
||||
viewModelOf(::CategoryViewModel)
|
||||
viewModelOf(::CountdownViewModel)
|
||||
viewModelOf(::FeedbackViewModel)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.taskttl.data.network
|
||||
|
||||
import com.taskttl.core.domain.constant.PointEvent
|
||||
import com.taskttl.core.domain.toJson
|
||||
import com.taskttl.core.network.ApiConfig
|
||||
import com.taskttl.core.network.KtorClient
|
||||
import com.taskttl.core.utils.DeviceUtils
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
import com.taskttl.data.network.domain.req.FeedbackReq
|
||||
import com.taskttl.data.network.domain.req.PointReq
|
||||
import com.taskttl.data.network.domain.resp.FeedbackResp
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.feedback_error
|
||||
|
||||
object TaskTTLApi {
|
||||
|
||||
/**
|
||||
* 发布反馈
|
||||
* @param [feedbackReq] 反馈请求
|
||||
* @return [String]
|
||||
*/
|
||||
suspend fun postFeedback(feedbackReq: FeedbackReq): FeedbackResp {
|
||||
try {
|
||||
feedbackReq.baseReq = DeviceUtils.getDeviceInfo()
|
||||
return KtorClient.postJson(ApiConfig.FEEDBACK_URL, feedbackReq.toJson())
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e("DevTTL",e.message.toString())
|
||||
throw Exception(getString(Res.string.feedback_error))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮递埋点
|
||||
* @param [point] 埋点
|
||||
* @return [PointReq]
|
||||
*/
|
||||
suspend fun postPoint(point: PointEvent) {
|
||||
try {
|
||||
val pointReq = PointReq(point.eventName, point.eventCode)
|
||||
pointReq.baseReq = DeviceUtils.getDeviceInfo()
|
||||
return KtorClient.postJson(ApiConfig.POINT_URL, pointReq.toJson())
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.taskttl.data.network.domain.req
|
||||
|
||||
import com.taskttl.core.domain.BaseReqWith
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 反馈请求
|
||||
* @author DevTTL
|
||||
* @date 2025/03/31
|
||||
* @constructor 创建[FeedbackReq]
|
||||
* @param [contactInfo] 联系方式
|
||||
* @param [contentBody] 反馈内容
|
||||
* @param [baseReq] 基础请求
|
||||
*/
|
||||
@Serializable
|
||||
data class FeedbackReq(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("contactInfo")
|
||||
val contactInfo: String,
|
||||
@SerialName("contentBody")
|
||||
val contentBody: String,
|
||||
) : BaseReqWith()
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.taskttl.data.network.domain.req
|
||||
|
||||
import com.taskttl.core.domain.BaseReqWith
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 埋点请求
|
||||
* @author admin
|
||||
* @date 2025/04/08
|
||||
* @constructor 创建[PointReq]
|
||||
* @param [eventName] 事件名称
|
||||
* @param [eventCode] 事件代码
|
||||
* @param [baseReq] 基本要求
|
||||
*/
|
||||
@Serializable
|
||||
data class PointReq(
|
||||
val eventName: String,
|
||||
val eventCode: Int,
|
||||
) : BaseReqWith()
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.taskttl.data.network.domain.resp
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 反馈响应
|
||||
* @author admin
|
||||
* @date 2025/10/09
|
||||
*/
|
||||
@Serializable
|
||||
object FeedbackResp
|
||||
@@ -51,6 +51,5 @@ sealed class CountdownIntent {
|
||||
*/
|
||||
sealed class CountdownEffect {
|
||||
data class ShowMessage(val message: String) : CountdownEffect()
|
||||
data class NavigateToCountdownDetail(val countdownId: String) : CountdownEffect()
|
||||
object NavigateBack : CountdownEffect()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.taskttl.data.state
|
||||
|
||||
import com.taskttl.data.network.domain.req.FeedbackReq
|
||||
|
||||
data class FeedbackState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
sealed class FeedbackIntent {
|
||||
/**
|
||||
* 清除错误
|
||||
* @author admin
|
||||
* @date 2025/09/27
|
||||
*/
|
||||
object ClearError : FeedbackIntent()
|
||||
|
||||
data class SubmitFeedback(var feedback: FeedbackReq) : FeedbackIntent()
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈效应
|
||||
* @author admin
|
||||
* @date 2025/10/12
|
||||
* @constructor 创建[FeedbackEffect]
|
||||
*/
|
||||
sealed class FeedbackEffect {
|
||||
/**
|
||||
* 导航返回
|
||||
* @author admin
|
||||
* @date 2025/10/12
|
||||
*/
|
||||
object NavigateBack : FeedbackEffect()
|
||||
|
||||
/**
|
||||
* 显示消息
|
||||
* @author admin
|
||||
* @date 2025/10/12
|
||||
* @constructor 创建[ShowMessage]
|
||||
* @param [message] 消息
|
||||
*/
|
||||
data class ShowMessage(val message: String) : FeedbackEffect()
|
||||
}
|
||||
@@ -15,6 +15,20 @@ 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
|
||||
import taskttl.composeapp.generated.resources.category_add_failed
|
||||
import taskttl.composeapp.generated.resources.category_add_success
|
||||
import taskttl.composeapp.generated.resources.category_count_update_failed
|
||||
import taskttl.composeapp.generated.resources.category_delete_failed
|
||||
import taskttl.composeapp.generated.resources.category_delete_success
|
||||
import taskttl.composeapp.generated.resources.category_init_failed
|
||||
import taskttl.composeapp.generated.resources.category_init_success
|
||||
import taskttl.composeapp.generated.resources.category_load_failed
|
||||
import taskttl.composeapp.generated.resources.category_not_found
|
||||
import taskttl.composeapp.generated.resources.category_stat_failed
|
||||
import taskttl.composeapp.generated.resources.category_update_failed
|
||||
import taskttl.composeapp.generated.resources.category_update_success
|
||||
|
||||
/**
|
||||
* 类别视图模型
|
||||
@@ -81,7 +95,7 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(
|
||||
isLoading = false,
|
||||
error = e.message ?: "加载分类失败"
|
||||
error = e.message ?: getString(Res.string.category_load_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -97,7 +111,8 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
val category = categoryRepository.getCategoryById(categoryId)
|
||||
_state.value = _state.value.copy(editingCategory = category)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "查询分类失败")
|
||||
_state.value =
|
||||
_state.value.copy(error = e.message ?: getString(Res.string.category_not_found))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +136,9 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "加载分类失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_load_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +153,9 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
_state.value = _state.value.copy(categoryStatistics = statistics)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "加载统计数据失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_stat_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,10 +176,12 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
categoryRepository.insertCategory(category)
|
||||
_effects.emit(CategoryEffect.ShowMessage("分类添加成功"))
|
||||
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_add_success)))
|
||||
_effects.emit(CategoryEffect.NavigateBack)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "添加分类失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_add_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,11 +190,13 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
categoryRepository.updateCategory(category)
|
||||
_effects.emit(CategoryEffect.ShowMessage("分类更新成功"))
|
||||
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_update_success)))
|
||||
_effects.emit(CategoryEffect.NavigateBack)
|
||||
hideEditDialog()
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "更新分类失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_update_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,10 +205,12 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
categoryRepository.deleteCategory(categoryId)
|
||||
_effects.emit(CategoryEffect.ShowMessage("分类删除成功"))
|
||||
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_delete_success)))
|
||||
hideDeleteDialog()
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "删除分类失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_delete_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,9 +219,11 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
categoryRepository.initializeDefaultCategories()
|
||||
_effects.emit(CategoryEffect.ShowMessage("默认分类初始化成功"))
|
||||
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_init_success)))
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "初始化默认分类失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_init_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +233,9 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
|
||||
try {
|
||||
categoryRepository.updateCategoryCounts()
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "更新分类计数失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.category_count_update_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ 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
|
||||
import taskttl.composeapp.generated.resources.countdown_add_failed
|
||||
import taskttl.composeapp.generated.resources.countdown_add_success
|
||||
import taskttl.composeapp.generated.resources.countdown_delete_failed
|
||||
import taskttl.composeapp.generated.resources.countdown_delete_success
|
||||
import taskttl.composeapp.generated.resources.countdown_load_failed
|
||||
import taskttl.composeapp.generated.resources.countdown_query_failed
|
||||
import taskttl.composeapp.generated.resources.countdown_update_failed
|
||||
import taskttl.composeapp.generated.resources.countdown_update_success
|
||||
|
||||
/**
|
||||
* 倒计时视图模型
|
||||
@@ -73,7 +83,10 @@ class CountdownViewModel(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.value =
|
||||
_state.value.copy(isLoading = false, error = e.message ?: "加载倒数日失败")
|
||||
_state.value.copy(
|
||||
isLoading = false,
|
||||
error = e.message ?: getString(Res.string.countdown_load_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +98,9 @@ class CountdownViewModel(
|
||||
val countdown = countdownRepository.getCountdownById(countdownId)
|
||||
_state.value = _state.value.copy(editingCountdown = countdown)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "查询倒数日失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.countdown_query_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,10 +109,12 @@ class CountdownViewModel(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
countdownRepository.insertCountdown(countdown)
|
||||
_effects.emit(CountdownEffect.ShowMessage("倒数日添加成功"))
|
||||
_effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_add_success)))
|
||||
_effects.emit(CountdownEffect.NavigateBack)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "添加倒数日失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.countdown_add_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,9 +123,11 @@ class CountdownViewModel(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
countdownRepository.updateCountdown(countdown)
|
||||
_effects.emit(CountdownEffect.ShowMessage("倒数日更新成功"))
|
||||
_effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_update_success)))
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "更新倒数日失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.countdown_update_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,9 +136,9 @@ class CountdownViewModel(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
countdownRepository.deleteCountdown(countdownId)
|
||||
_effects.emit(CountdownEffect.ShowMessage("倒数日删除成功"))
|
||||
_effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_delete_success)))
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "删除倒数日失败")
|
||||
_state.value = _state.value.copy(error = e.message ?: getString(Res.string.countdown_delete_failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.taskttl.data.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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
|
||||
import taskttl.composeapp.generated.resources.feedback_error
|
||||
import taskttl.composeapp.generated.resources.feedback_success
|
||||
|
||||
/**
|
||||
* 反馈视图模型
|
||||
* @author admin
|
||||
* @date 2025/10/12
|
||||
* @constructor 创建[FeedbackViewModel]
|
||||
*/
|
||||
class FeedbackViewModel() : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(FeedbackState())
|
||||
val state: StateFlow<FeedbackState> = _state.asStateFlow()
|
||||
|
||||
private val _effects = MutableSharedFlow<FeedbackEffect>()
|
||||
val effects: SharedFlow<FeedbackEffect> = _effects.asSharedFlow()
|
||||
|
||||
|
||||
fun handleIntent(intent: FeedbackIntent) {
|
||||
when (intent) {
|
||||
is FeedbackIntent.SubmitFeedback -> submitFeedback(intent.feedback)
|
||||
is FeedbackIntent.ClearError -> clearError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitFeedback(feedback: FeedbackReq) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||
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)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(isLoading = false, error = e.message)
|
||||
_effects.emit(
|
||||
FeedbackEffect.ShowMessage(
|
||||
e.message ?: getString(Res.string.feedback_error)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误
|
||||
*/
|
||||
private fun clearError() {
|
||||
_state.value = _state.value.copy(error = null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,9 +2,13 @@ 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.data.network.TaskTTLApi
|
||||
import com.taskttl.data.repository.OnboardingRepository
|
||||
import com.taskttl.data.state.SplashState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -26,12 +30,18 @@ class SplashViewModel(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
delay(1200) // 模拟启动等待
|
||||
DeviceUtils.getUniqueId()
|
||||
|
||||
val firstResult = StorageUtils.getBoolean(PointEvent.FirstAppLaunch.eventName, false)
|
||||
if (!firstResult) {
|
||||
TaskTTLApi.postPoint(PointEvent.FirstAppLaunch)
|
||||
StorageUtils.saveBoolean(PointEvent.FirstAppLaunch.eventName, true)
|
||||
} else {
|
||||
TaskTTLApi.postPoint(PointEvent.AppLaunch)
|
||||
}
|
||||
val hasLaunched = onboardingRepository.isLaunchedBefore()
|
||||
_uiState.value =
|
||||
if (hasLaunched) SplashState.NavigateToOnboarding else SplashState.NavigateToMain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,18 @@ 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
|
||||
import taskttl.composeapp.generated.resources.task_add_failed
|
||||
import taskttl.composeapp.generated.resources.task_add_success
|
||||
import taskttl.composeapp.generated.resources.task_delete_failed
|
||||
import taskttl.composeapp.generated.resources.task_delete_success
|
||||
import taskttl.composeapp.generated.resources.task_load_failed
|
||||
import taskttl.composeapp.generated.resources.task_query_failed
|
||||
import taskttl.composeapp.generated.resources.task_status_update_failed
|
||||
import taskttl.composeapp.generated.resources.task_status_update_success
|
||||
import taskttl.composeapp.generated.resources.task_update_failed
|
||||
import taskttl.composeapp.generated.resources.task_update_success
|
||||
|
||||
/**
|
||||
* 任务视图模型
|
||||
@@ -85,10 +97,11 @@ class TaskViewModel(
|
||||
_state.value = _state.value.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)
|
||||
}
|
||||
categoryRepository.getCategoriesByType(CategoryType.TASK)
|
||||
.collect { categories ->
|
||||
LogUtils.e("DevTTL", categories.toString())
|
||||
_state.value = _state.value.copy(categories = categories)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
taskRepository.getAllTasks().collect { tasks ->
|
||||
@@ -100,9 +113,12 @@ class TaskViewModel(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e("DevTTL",e.message.toString())
|
||||
LogUtils.e("DevTTL", e.message.toString())
|
||||
_state.value =
|
||||
_state.value.copy(isLoading = false, error = e.message ?: "加载任务失败")
|
||||
_state.value.copy(
|
||||
isLoading = false,
|
||||
error = e.message ?: getString(Res.string.task_load_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +133,8 @@ class TaskViewModel(
|
||||
val task = taskRepository.getTaskById(taskId)
|
||||
_state.value = _state.value.copy(editingTask = task)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "查询任务失败")
|
||||
_state.value =
|
||||
_state.value.copy(error = e.message ?: getString(Res.string.task_query_failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,10 +147,11 @@ class TaskViewModel(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
taskRepository.insertTask(task)
|
||||
_effects.emit(TaskEffect.ShowMessage("任务添加成功"))
|
||||
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_add_success)))
|
||||
_effects.emit(TaskEffect.NavigateBack)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "添加任务失败")
|
||||
_state.value =
|
||||
_state.value.copy(error = e.message ?: getString(Res.string.task_add_failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,10 +164,11 @@ class TaskViewModel(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
taskRepository.updateTask(task)
|
||||
_effects.emit(TaskEffect.ShowMessage("任务更新成功"))
|
||||
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_update_success)))
|
||||
_effects.emit(TaskEffect.NavigateBack)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "更新任务失败")
|
||||
_state.value =
|
||||
_state.value.copy(error = e.message ?: getString(Res.string.task_update_failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,9 +181,10 @@ class TaskViewModel(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
taskRepository.deleteTask(taskId)
|
||||
_effects.emit(TaskEffect.ShowMessage("任务删除成功"))
|
||||
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "删除任务失败")
|
||||
_state.value =
|
||||
_state.value.copy(error = e.message ?: getString(Res.string.task_delete_failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,9 +200,12 @@ class TaskViewModel(
|
||||
task?.let {
|
||||
val updatedTask = it.copy(isCompleted = !it.isCompleted)
|
||||
taskRepository.updateTask(updatedTask)
|
||||
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_status_update_success)))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = e.message ?: "更新任务状态失败")
|
||||
_state.value = _state.value.copy(
|
||||
error = e.message ?: getString(Res.string.task_status_update_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +48,15 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryColor
|
||||
import com.taskttl.data.local.model.CategoryIcon
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.state.CategoryEffect
|
||||
import com.taskttl.data.state.CategoryIntent
|
||||
import com.taskttl.data.state.TaskEffect
|
||||
import com.taskttl.data.viewmodel.CategoryViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import kotlinx.datetime.TimeZone
|
||||
@@ -88,6 +91,17 @@ fun CategoryEditScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CategoryViewModel = koinViewModel()
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CategoryEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(categoryId) {
|
||||
categoryId?.let { viewModel.handleIntent(CategoryIntent.GetCategoryById(it)) }
|
||||
}
|
||||
@@ -269,6 +283,7 @@ fun CategoryEditScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,10 +55,15 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.routes.Routes.Main
|
||||
import com.taskttl.core.ui.ActionButtonListItem
|
||||
import com.taskttl.core.ui.ErrorDialog
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
import com.taskttl.data.state.CategoryEffect
|
||||
import com.taskttl.data.state.CategoryIntent
|
||||
import com.taskttl.data.state.TaskEffect
|
||||
import com.taskttl.data.state.TaskIntent
|
||||
import com.taskttl.data.viewmodel.CategoryViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -66,7 +71,7 @@ import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.label_add_category_hint
|
||||
import taskttl.composeapp.generated.resources.label_countdown_count
|
||||
import taskttl.composeapp.generated.resources.label_edit
|
||||
import taskttl.composeapp.generated.resources.edit
|
||||
import taskttl.composeapp.generated.resources.label_no_category
|
||||
import taskttl.composeapp.generated.resources.label_task_count
|
||||
import taskttl.composeapp.generated.resources.title_add_category
|
||||
@@ -85,6 +90,9 @@ fun CategoryScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CategoryEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
is CategoryEffect.NavigateBack -> {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
@@ -96,7 +104,10 @@ fun CategoryScreen(
|
||||
|
||||
// 错误处理
|
||||
state.error?.let { error ->
|
||||
LaunchedEffect(error) { viewModel.handleIntent(CategoryIntent.ClearError) }
|
||||
ErrorDialog(
|
||||
errorMessage = state.error,
|
||||
onDismiss = { viewModel.handleIntent(CategoryIntent.ClearError) }
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -166,7 +177,7 @@ fun CategoryScreen(
|
||||
item { Spacer(Modifier) }
|
||||
itemsIndexed(
|
||||
items = categories,
|
||||
key = { index, item -> item.name }) { index, category ->
|
||||
key = { index, item -> item.id }) { index, category ->
|
||||
var isOpen by remember { mutableStateOf(false) }
|
||||
|
||||
CategoryCardItem(
|
||||
@@ -199,8 +210,7 @@ fun CategoryScreen(
|
||||
contentDescription = stringResource(Res.string.title_add_category)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +298,7 @@ fun CategoryCardItem(
|
||||
IconButton(onClick = onEditClick) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = stringResource(Res.string.label_edit),
|
||||
contentDescription = stringResource(Res.string.edit),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.ui.Chip
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.DateUtils
|
||||
import com.taskttl.data.state.CountdownEffect
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
@@ -41,9 +42,9 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.countdown_not_found
|
||||
import taskttl.composeapp.generated.resources.created_at
|
||||
import taskttl.composeapp.generated.resources.detail_information
|
||||
import taskttl.composeapp.generated.resources.event_description
|
||||
import taskttl.composeapp.generated.resources.label_created_at
|
||||
import taskttl.composeapp.generated.resources.label_days
|
||||
import taskttl.composeapp.generated.resources.reminder
|
||||
import taskttl.composeapp.generated.resources.title_countdown_info
|
||||
@@ -225,7 +226,7 @@ fun CountdownDetailScreen(
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
InfoItem(
|
||||
iconTint = Color(0xFF999999),
|
||||
text = "${stringResource(Res.string.created_at)}:${countdown.createdAt}",
|
||||
text = "${stringResource(Res.string.label_created_at)}${countdown.createdAt}",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -233,6 +234,7 @@ fun CountdownDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.data.local.model.ReminderFrequency
|
||||
@@ -59,10 +61,10 @@ import taskttl.composeapp.generated.resources.desc_select_date
|
||||
import taskttl.composeapp.generated.resources.label_countdown_description
|
||||
import taskttl.composeapp.generated.resources.label_countdown_title
|
||||
import taskttl.composeapp.generated.resources.label_notification_setting
|
||||
import taskttl.composeapp.generated.resources.label_select_category
|
||||
import taskttl.composeapp.generated.resources.label_target_date
|
||||
import taskttl.composeapp.generated.resources.title_add_countdown
|
||||
import taskttl.composeapp.generated.resources.title_edit_countdown
|
||||
import taskttl.composeapp.generated.resources.title_select_category
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
@@ -75,6 +77,17 @@ fun CountdownEditScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CountdownViewModel = koinViewModel()
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CountdownEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(countdownId) {
|
||||
countdownId?.let { viewModel.handleIntent(CountdownIntent.GetCountdownById(it)) }
|
||||
}
|
||||
@@ -178,7 +191,7 @@ fun CountdownEditScreen(
|
||||
|
||||
// 分类选择
|
||||
Text(
|
||||
text = stringResource(Res.string.label_select_category),
|
||||
text = stringResource(Res.string.title_select_category),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
@@ -270,5 +283,6 @@ fun CountdownEditScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,13 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.ui.ErrorDialog
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.DateUtils
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.data.state.CategoryEffect
|
||||
import com.taskttl.data.state.CategoryIntent
|
||||
import com.taskttl.data.state.CountdownEffect
|
||||
import com.taskttl.data.state.CountdownIntent
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
@@ -72,7 +77,7 @@ import taskttl.composeapp.generated.resources.delete
|
||||
import taskttl.composeapp.generated.resources.desc_add_countdown
|
||||
import taskttl.composeapp.generated.resources.label_countdown_list
|
||||
import taskttl.composeapp.generated.resources.label_days
|
||||
import taskttl.composeapp.generated.resources.label_edit
|
||||
import taskttl.composeapp.generated.resources.edit
|
||||
import taskttl.composeapp.generated.resources.text_add_countdown_tip
|
||||
import taskttl.composeapp.generated.resources.text_no_countdowns
|
||||
import taskttl.composeapp.generated.resources.title_countdown
|
||||
@@ -89,23 +94,21 @@ fun CountdownScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CountdownEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
is CountdownEffect.NavigateBack -> {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
is CountdownEffect.NavigateToCountdownDetail -> {
|
||||
// onNavigateToCountdownDetail(effect.countdownId)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
LaunchedEffect(error) {
|
||||
viewModel.handleIntent(CountdownIntent.ClearError)
|
||||
}
|
||||
ErrorDialog(
|
||||
errorMessage = state.error,
|
||||
onDismiss = { viewModel.handleIntent(CountdownIntent.ClearError) }
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -217,6 +220,7 @@ fun CountdownScreen(
|
||||
contentDescription = stringResource(Res.string.desc_add_countdown)
|
||||
)
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +360,7 @@ fun CountdownCard(
|
||||
onDismissRequest = { showMoreMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.label_edit)) },
|
||||
text = { Text(stringResource(Res.string.edit)) },
|
||||
onClick = {
|
||||
onEdit.invoke();
|
||||
showMoreMenu = false
|
||||
|
||||
@@ -51,6 +51,7 @@ import taskttl.composeapp.generated.resources.tech_stack
|
||||
import taskttl.composeapp.generated.resources.tech_stack_compose
|
||||
import taskttl.composeapp.generated.resources.tech_stack_kmp
|
||||
import taskttl.composeapp.generated.resources.tech_stack_koin
|
||||
import taskttl.composeapp.generated.resources.tech_stack_ktor
|
||||
import taskttl.composeapp.generated.resources.tech_stack_mvi
|
||||
import taskttl.composeapp.generated.resources.tech_stack_room
|
||||
import taskttl.composeapp.generated.resources.title_about
|
||||
@@ -165,6 +166,7 @@ fun AboutScreen(
|
||||
TechStackItem("Jetpack Compose", Res.string.tech_stack_compose)
|
||||
TechStackItem("Room Database", Res.string.tech_stack_room)
|
||||
TechStackItem("Koin", Res.string.tech_stack_koin)
|
||||
TechStackItem("Ktor", Res.string.tech_stack_ktor)
|
||||
TechStackItem("MVI Architecture", Res.string.tech_stack_mvi)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ import taskttl.composeapp.generated.resources.desc_import_data
|
||||
import taskttl.composeapp.generated.resources.export
|
||||
import taskttl.composeapp.generated.resources.import
|
||||
import taskttl.composeapp.generated.resources.label_csv_format
|
||||
import taskttl.composeapp.generated.resources.label_enter
|
||||
import taskttl.composeapp.generated.resources.enter
|
||||
import taskttl.composeapp.generated.resources.label_json_format
|
||||
import taskttl.composeapp.generated.resources.label_select_export_format
|
||||
import taskttl.composeapp.generated.resources.label_select_file
|
||||
@@ -275,7 +275,7 @@ private fun DataManagementCard(
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = stringResource(Res.string.label_enter),
|
||||
contentDescription = stringResource(Res.string.enter),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -39,11 +41,20 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.core.domain.FeedbackType
|
||||
import com.taskttl.core.ui.ErrorDialog
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
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.TaskIntent
|
||||
import com.taskttl.data.viewmodel.FeedbackViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.button_cancel
|
||||
import taskttl.composeapp.generated.resources.button_send_feedback
|
||||
import taskttl.composeapp.generated.resources.cancel
|
||||
import taskttl.composeapp.generated.resources.feedback_contact
|
||||
import taskttl.composeapp.generated.resources.feedback_contact_placeholder
|
||||
import taskttl.composeapp.generated.resources.feedback_description
|
||||
@@ -55,12 +66,31 @@ import taskttl.composeapp.generated.resources.title_feedback
|
||||
@Composable
|
||||
fun FeedbackScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onSubmit: () -> Unit
|
||||
viewModel: FeedbackViewModel = koinViewModel()
|
||||
) {
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is FeedbackEffect.NavigateBack -> onNavigateBack.invoke()
|
||||
is FeedbackEffect.ShowMessage -> ToastUtils.show(effect.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var feedbackType by remember { mutableStateOf(FeedbackType.ISSUE) }
|
||||
var contact by remember { mutableStateOf("") }
|
||||
var description by remember { mutableStateOf("") }
|
||||
|
||||
state.error?.let { error ->
|
||||
ErrorDialog(
|
||||
errorMessage = state.error,
|
||||
onDismiss = { viewModel.handleIntent(FeedbackIntent.ClearError) }
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -71,7 +101,9 @@ fun FeedbackScreen(
|
||||
onBackClick = { onNavigateBack.invoke() },
|
||||
trailingIcon = Icons.AutoMirrored.Filled.Send,
|
||||
onTrailingClick = {
|
||||
onSubmit()
|
||||
if (contact.isNotBlank()) {
|
||||
submitFeedback(viewModel, feedbackType, contact, description)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -167,7 +199,9 @@ fun FeedbackScreen(
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
onSubmit()
|
||||
if (contact.isNotBlank()) {
|
||||
submitFeedback(viewModel, feedbackType, contact, description)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
@@ -177,13 +211,14 @@ fun FeedbackScreen(
|
||||
onClick = { onNavigateBack.invoke() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(Res.string.button_cancel))
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,3 +248,17 @@ private fun FeedbackTypeOption(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitFeedback(
|
||||
viewModel: FeedbackViewModel,
|
||||
type: FeedbackType,
|
||||
contact: String,
|
||||
description: String
|
||||
) {
|
||||
val feedback = FeedbackReq(
|
||||
type = type.code,
|
||||
contactInfo = contact,
|
||||
contentBody = description.trim()
|
||||
)
|
||||
viewModel.handleIntent(FeedbackIntent.SubmitFeedback(feedback))
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package com.taskttl.presentation.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.taskttl.core.ui.DevTTLWebView
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.privacy_url
|
||||
import taskttl.composeapp.generated.resources.title_about
|
||||
import taskttl.composeapp.generated.resources.title_privacy
|
||||
|
||||
@Composable
|
||||
fun PrivacyScreen(onNavigateBack: () -> Unit) {
|
||||
@@ -26,7 +21,7 @@ fun PrivacyScreen(onNavigateBack: () -> Unit) {
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
AppHeader(
|
||||
title = Res.string.title_about,
|
||||
title = Res.string.title_privacy,
|
||||
showBack = true,
|
||||
onBackClick = { onNavigateBack.invoke() }
|
||||
)
|
||||
@@ -34,10 +29,7 @@ fun PrivacyScreen(onNavigateBack: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F5F5))
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
.background(Color(0xFFF5F5F5)),
|
||||
) {
|
||||
DevTTLWebView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.ui.Chip
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.state.CountdownIntent
|
||||
import com.taskttl.data.state.TaskIntent
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.data.state.TaskEffect
|
||||
import com.taskttl.data.state.TaskIntent
|
||||
import com.taskttl.data.viewmodel.TaskViewModel
|
||||
@@ -228,5 +229,6 @@ fun TaskDetailScreen(
|
||||
}
|
||||
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.Task
|
||||
import com.taskttl.data.local.model.TaskPriority
|
||||
@@ -78,6 +81,18 @@ fun TaskEditorScreen(
|
||||
viewModel: TaskViewModel = koinViewModel()
|
||||
) {
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is TaskEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(taskId) {
|
||||
taskId?.let { viewModel.handleIntent(TaskIntent.GetTaskById(it)) }
|
||||
}
|
||||
@@ -277,5 +292,6 @@ fun TaskEditorScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.ui.ActionButtonListItem
|
||||
import com.taskttl.core.ui.ErrorDialog
|
||||
import com.taskttl.core.ui.LoadingScreen
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Task
|
||||
import com.taskttl.data.state.TaskEffect
|
||||
import com.taskttl.data.state.TaskIntent
|
||||
@@ -90,6 +93,10 @@ fun TaskScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is TaskEffect.ShowMessage -> {
|
||||
ToastUtils.show(effect.message)
|
||||
}
|
||||
|
||||
is TaskEffect.NavigateToTaskDetail -> {
|
||||
navController.navigate(Routes.Main.Task.TaskDetail(effect.taskId))
|
||||
}
|
||||
@@ -100,7 +107,10 @@ fun TaskScreen(
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
LaunchedEffect(error) { viewModel.handleIntent(TaskIntent.ClearError) }
|
||||
ErrorDialog(
|
||||
errorMessage = state.error,
|
||||
onDismiss = { viewModel.handleIntent(TaskIntent.ClearError) }
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -207,7 +217,7 @@ fun TaskScreen(
|
||||
item { Spacer(Modifier) }
|
||||
itemsIndexed(
|
||||
state.filteredTasks,
|
||||
key = { index, item -> item.title }) { index, task ->
|
||||
key = { index, item -> item.id }) { index, task ->
|
||||
var isOpen by remember { mutableStateOf(false) }
|
||||
|
||||
TaskCardItem(
|
||||
@@ -248,6 +258,7 @@ fun TaskScreen(
|
||||
contentDescription = stringResource(Res.string.title_add_task)
|
||||
)
|
||||
}
|
||||
if (state.isLoading) LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user