From ede72fdedcf989934c156b3b8b3b5872dfe4e062 Mon Sep 17 00:00:00 2001 From: devttl Date: Sun, 12 Oct 2025 22:08:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composeApp/build.gradle.kts | 43 ++- .../src/androidMain/AndroidManifest.xml | 8 +- .../core/network/KtorClient.android.kt | 13 + .../taskttl/core/ui/DevTTLWebView.android.kt | 4 +- .../taskttl/core/utils/DeviceUtils.android.kt | 139 ++++++++++ .../taskttl/core/utils/LogUtils.android.kt | 4 +- .../res/xml/network_security_config.xml | 6 + .../composeResources/values/strings.xml | 260 +++++++++--------- .../src/commonMain/kotlin/com/taskttl/App.kt | 3 +- .../core/config/FlattenBaseReqSerializer.kt | 86 ++++++ .../com/taskttl/core/domain/ApiResponse.kt | 11 + .../kotlin/com/taskttl/core/domain/BaseReq.kt | 58 ++++ .../com/taskttl/core/domain/FeedbackType.kt | 6 +- .../core/domain/constant/PointEvent.kt | 48 ++++ .../com/taskttl/core/network/ApiConfig.kt | 18 ++ .../com/taskttl/core/network/KtorClient.kt | 106 +++++++ .../kotlin/com/taskttl/core/routes/MainNav.kt | 5 +- .../kotlin/com/taskttl/core/ui/ErrorDialog.kt | 31 +++ .../com/taskttl/core/ui/LoadingScreen.kt | 30 ++ .../com/taskttl/core/utils/DeviceUtils.kt | 25 ++ .../com/taskttl/core/utils/JsonUtils.kt | 17 ++ .../kotlin/com/taskttl/core/utils/LogUtils.kt | 4 +- .../com/taskttl/core/utils/ToastUtils.kt | 5 + .../kotlin/com/taskttl/data/di/KoinModels.kt | 12 +- .../com/taskttl/data/network/TaskTTLApi.kt | 46 ++++ .../data/network/domain/req/FeedbackReq.kt | 24 ++ .../data/network/domain/req/PointReq.kt | 21 ++ .../data/network/domain/resp/FeedbackResp.kt | 11 + .../com/taskttl/data/state/CountdownState.kt | 1 - .../com/taskttl/data/state/FeedbackState.kt | 43 +++ .../data/viewmodel/CategoryViewModel.kt | 55 +++- .../data/viewmodel/CountdownViewModel.kt | 35 ++- .../data/viewmodel/FeedbackViewModel.kt | 73 +++++ .../taskttl/data/viewmodel/SplashViewModel.kt | 18 +- .../taskttl/data/viewmodel/TaskViewModel.kt | 51 +++- .../category/CategoryEditorScreen.kt | 15 + .../presentation/category/CategoryScreen.kt | 22 +- .../countdown/CountdownDetailScreen.kt | 6 +- .../countdown/CountdownEditorScreen.kt | 18 +- .../presentation/countdown/CountdownScreen.kt | 26 +- .../presentation/settings/AboutScreen.kt | 2 + .../settings/DataManagementScreen.kt | 4 +- .../presentation/settings/FeedbackScreen.kt | 59 +++- .../presentation/settings/PrivacyScreen.kt | 14 +- .../statistics/StatisticsScreen.kt | 1 + .../presentation/task/TaskDetailScreen.kt | 2 + .../presentation/task/TaskEditorScreen.kt | 16 ++ .../taskttl/presentation/task/TaskScreen.kt | 15 +- .../taskttl/core/network/KtorClient.ios.kt | 10 + .../com/taskttl/core/utils/LogUtils.ios.kt | 3 +- gradle.properties | 4 +- gradle/libs.versions.toml | 35 ++- 52 files changed, 1325 insertions(+), 247 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/com/taskttl/core/network/KtorClient.android.kt create mode 100644 composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt create mode 100644 composeApp/src/androidMain/res/xml/network_security_config.xml create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/domain/ApiResponse.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/domain/constant/PointEvent.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/ErrorDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/LoadingScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/utils/DeviceUtils.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/utils/JsonUtils.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/network/TaskTTLApi.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/FeedbackReq.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/PointReq.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/resp/FeedbackResp.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt create mode 100644 composeApp/src/iosMain/kotlin/com/taskttl/core/network/KtorClient.ios.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f31dd33..a3411d2 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { jvmTarget.set(JvmTarget.JVM_11) } } - + listOf( iosX64(), iosArm64(), @@ -33,7 +33,7 @@ kotlin { isStatic = true } } - + jvm() js { @@ -46,7 +46,7 @@ kotlin { browser() binaries.executable() } - + sourceSets { androidMain.dependencies { implementation(compose.preview) @@ -55,6 +55,9 @@ kotlin { // Koin依赖注入 implementation(libs.koin.android) + // Ktor网络请求 + implementation(libs.ktor.client.android) + // firebase implementation(project.dependencies.platform(libs.firebase.bom)) implementation(libs.firebase.analytics) @@ -66,6 +69,9 @@ kotlin { // sqlite implementation(libs.androidx.room.sqlite.wrapper) + + // admob + implementation(libs.android.play.services.ads.identifier) } commonMain.dependencies { implementation(compose.runtime) @@ -86,6 +92,12 @@ kotlin { implementation(libs.koin.compose) implementation(libs.koin.viewmodel) + // Ktor网络请求 + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + // coil implementation(libs.coil3.compose) implementation(libs.coil3.svg) @@ -106,6 +118,13 @@ kotlin { implementation(libs.androidx.room.runtime) implementation(libs.androidx.sqlite.bundled) } + iosMain.dependencies { + // ktor网络请求 + implementation(libs.ktor.client.darwin) + + // implementation(libs.kotlinx.coroutines.core) + // implementation(libs.kotlinx.coroutines.core.native) + } jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) @@ -127,8 +146,11 @@ android { versionCode = 1 versionName = "1.0" + buildConfigField("String", "APP_NAME", "\"TaskTTL\"") + manifestPlaceholders["facebookAppId"] = libs.versions.android.facebookAppId.get() - manifestPlaceholders["facebookClientToken"] = libs.versions.android.facebookClientToken.get() + manifestPlaceholders["facebookClientToken"] = + libs.versions.android.facebookClientToken.get() } packaging { resources { @@ -137,7 +159,20 @@ android { } buildTypes { getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + + buildConfigField("Boolean", "DEBUG", "false") + buildConfigField("Integer", "APP_ID", "1") + buildConfigField("Integer", "VERSION_CODE", libs.versions.android.versionCode.get()) + } + + getByName("debug") { isMinifyEnabled = false + + buildConfigField("Boolean", "DEBUG", "true") + buildConfigField("Integer", "APP_ID", "999") + buildConfigField("Integer", "VERSION_CODE", libs.versions.android.versionCode.get()) } } compileOptions { diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 2e80973..1d92678 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,10 +1,12 @@ - + - + () + // 设备硬件信息 + deviceIdentifiers.add(buildDeviceInfo()) + return hashString(deviceIdentifiers.joinToString("|")) + } + + private fun buildDeviceInfo(): String { + return StringBuilder().apply { + append(Build.BOARD).append(":") + append(Build.BRAND).append(":") + append(Build.DEVICE).append(":") + append(Build.HARDWARE).append(":") + append(Build.MANUFACTURER).append(":") + append(Build.MODEL).append(":") + append(Build.PRODUCT) + }.toString() + } + + private fun hashString(input: String): String { + return try { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) + // 返回前64字符 + bytesToHex(hash).take(64) + } catch (e: Exception) { + UUID.randomUUID().toString().replace("-", "").take(64) + } + } + + private fun bytesToHex(bytes: ByteArray): String { + val hexChars = "0123456789abcdef" + return StringBuilder(bytes.size * 2).apply { + bytes.forEach { byte -> + val i = byte.toInt() and 0xff + append(hexChars[i shr 4]) + append(hexChars[i and 0x0f]) + } + }.toString() + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/LogUtils.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/LogUtils.android.kt index eb8b1e5..9986150 100644 --- a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/LogUtils.android.kt +++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/LogUtils.android.kt @@ -1,5 +1,7 @@ package com.taskttl.core.utils +import com.taskttl.BuildConfig +import io.ktor.client.plugins.logging.Logger import android.util.Log as AndroidLog /** @@ -23,4 +25,4 @@ actual object LogUtils { actual fun e(tag: String, message: String, throwable: Throwable?) { AndroidLog.e(tag, message, throwable) } -} \ No newline at end of file +} diff --git a/composeApp/src/androidMain/res/xml/network_security_config.xml b/composeApp/src/androidMain/res/xml/network_security_config.xml new file mode 100644 index 0000000..b40dcb3 --- /dev/null +++ b/composeApp/src/androidMain/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 10.0.0.5 + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 011bb43..6d453db 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1,27 +1,38 @@ + + 继续 + 跳过 + 开始使用 + + 欢迎使用 TaskTTL + 一个简洁而强大的任务管理工具,帮助您高效管理日常任务和重要日期 + 智能任务管理 + 创建、分类和跟踪您的任务。设置优先级,添加截止日期,让工作更有条理 + 重要日期提醒 + 设置重要日期的倒数计时,永远不会错过生日、纪念日或重要的截止日期 + 准备就绪! + 现在您可以开始创建第一个任务,让我们一起提高工作效率吧! + + + TaskTTL + 任务管理与倒数日应用 + 让每一天都更有意义 + 版本 + 构建版本 + + 待办 倒数日 统计 设置 + + 搜索任务... 我的任务 任务详情 添加任务 编辑任务 - 倒数日 - 倒数日详情 - 添加倒数日 - 编辑倒数日 - - 统计 - - 关于 - - 分类管理 - 添加分类 - 编辑分类 - 任务列表 显示已完成 暂无任务 @@ -44,6 +55,23 @@ 创建时间: 任务描述 + 任务添加成功 + 添加任务失败 + 任务更新成功 + 更新任务失败 + 任务删除成功 + 删除任务失败 + 加载任务失败 + 查询任务失败 + 更新任务状态成功 + 更新任务状态失败 + + + 倒数日 + 倒数日详情 + 添加倒数日 + 编辑倒数日 + 倒数日列表 暂无倒数日 @@ -52,46 +80,86 @@ 倒数日标题 倒数日描述 - 选择分类 目标日期 通知设置 + 倒数日不存在 + 事件描述 + 详细信息 + 提醒 + 倒数日添加成功 + 添加倒数日失败 + 倒数日更新成功 + 更新倒数日失败 + 倒数日删除成功 + 删除倒数日失败 + 加载倒数日失败 + 查询倒数日失败 + + + 统计 + 总览 + 分类统计 总任务 已完成 完成率 倒数日总数 活跃中 - 总览 - 分类统计 + + 任务 + 倒数日 - - 倒数日不存在 + 分类管理 + 添加分类 + 编辑分类 + 暂无分类 + 点击右下角按钮添加新分类 + 分类名称 + 输入分类名称... + 分类类型 + 选择颜色 + 选择图标 + 任务分类 + 倒数日分类 - - - 事件描述 - 详细信息 + %1$d 个任务 + %1$d 个倒数日 - - 提醒 - 创建时间 + + 分类添加成功 + 添加分类失败 + 分类更新成功 + 更新分类失败 + 分类删除成功 + 删除分类失败 + 加载分类失败 + 加载统计数据失败 + 默认分类初始化成功 + 初始化默认分类失败 + 查询分类失败 + 更新分类计数失败 - + + 进入 + 编辑 取消 确定 删除 导出 导入 - 选择文件 返回 操作 - 全部 - 搜索任务... 搜索 清除 + 全部 + 重试 + 选择文件 + 错误 + 加载失败,请检查网络连接 - + + 数据管理 导出数据 将所有任务和倒数日导出为文件 @@ -101,45 +169,27 @@ 定期自动备份数据到云端 清除所有数据 删除所有任务、倒数日和设置 - 清理已完成任务 - 删除所有已完成的任务 - 清理过期倒数日 - 删除所有已过期的倒数日 此操作将删除所有任务、倒数日和设置,且无法恢复。 + 清理已完成任务 + 清理过期倒数日 + 删除所有已完成的任务 + 删除所有已过期的倒数日 - 选择要导入的文件 - 进入 - 数据清理 备份与恢复 - - + 数据清理 + 选择要导入的文件 选择文件 选择导出格式 JSON格式 CSV格式 - 暂无分类 - 点击右下角按钮添加新分类 - 编辑 - %1$d 个任务 - %1$d 个倒数日 - - 分类名字 - 输入分类名称... - 分类类型 - 选择颜色 - 选择图标 - 任务分类 - 倒数日分类 - - + 应用设置 通用设置 数据管理 社交分享 帮助与反馈 - 推送通知 接收任务和倒数日提醒 深色模式 @@ -147,19 +197,16 @@ 语言设置 简体中文 - 分类管理 管理分类 数据管理 备份和恢复数据 - 分享成就 分享任务完成成就 推荐给朋友 邀请朋友使用 TaskMaster - 意见反馈 告诉我们您的想法 隐私政策 @@ -167,95 +214,47 @@ 关于应用 版本 1.0.0 - - TaskMaster 用户 - 已使用 %1$d 天 · 完成 %2$d 个任务 + + 意见反馈 + 反馈类型 + 问题反馈 + 功能建议 + 问题描述 + 请详细描述您遇到的问题或建议... + 联系方式(可选) + 您的邮箱地址,方便我们回复 + 感谢您的反馈!我们会尽快处理。 + 请填写反馈内容 + 发送反馈 - - 输入分类名称... - - - - 版本 - 构建版本 - - + + 关于 应用介绍 - TaskTTL 是一款现代化的任务管理与倒数日应用,帮助您高效管理日常任务与重要日期。 - 支持分类管理、优先级设置与统计分析,让生活更有条理。 + TaskTTL 是一款现代化的任务管理与倒数日应用, + 支持分类管理、优先级设置与统计分析,让生活更有条理。 - + 隐私协议 + 技术栈 Kotlin Multiplatform(跨平台开发框架) Jetpack Compose(现代化 UI 框架) Room Database(本地存储) Koin(依赖注入框架) + Ktor(网络请求) MVI Architecture(响应式架构模式) - 开发者 DevTTL 团队 - - 联系我们 电子邮箱 - team@devttl.com + admin@devttl.com 官方网站 https://devttl.com - - 2025 保留所有权利 - 发送反馈 - 取消 - - - 意见反馈 - - - 反馈类型 - 问题反馈 - 功能建议 - - - 问题描述 - 请详细描述您遇到的问题或建议... - - - 联系方式(可选) - 您的邮箱地址,方便我们回复 - - - 感谢您的反馈!我们会尽快处理。 - 请填写反馈内容 - - - TaskMaster - 任务管理与倒数日应用 - 让每一天都更有意义 - - 继续 - 跳过 - 开始使用 - - 欢迎使用 TaskMaster - 一个简洁而强大的任务管理工具,帮助您高效管理日常任务和重要日期 - - 智能任务管理 - 创建、分类和跟踪您的任务。设置优先级,添加截止日期,让工作更有条理 - - 重要日期提醒 - 设置重要日期的倒数计时,永远不会错过生日、纪念日或重要的截止日期 - - 准备就绪! - 现在您可以开始创建第一个任务,让我们一起提高工作效率吧! - - 任务 - 倒数日 - @@ -306,8 +305,9 @@ 目标 提醒 - 加载失败,请检查网络连接 - 重试 - https://devttl.com + https://sites.google.com/view/taskttl/privacy - \ No newline at end of file + 反馈成功 + 反馈失败,请检查网络连接或稍后重试 + + diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/App.kt b/composeApp/src/commonMain/kotlin/com/taskttl/App.kt index 426b156..773a9ce 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/App.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/App.kt @@ -14,6 +14,5 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun App() { AppTheme { AppNav() - } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt new file mode 100644 index 0000000..b19a536 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt @@ -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( + private val valueSerializer: KSerializer, + private val baseSelector: (T) -> B +) : KSerializer { + + 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 +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/ApiResponse.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/ApiResponse.kt new file mode 100644 index 0000000..c48d1ce --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/ApiResponse.kt @@ -0,0 +1,11 @@ +package com.taskttl.core.domain + +import kotlinx.serialization.Serializable + +/** + * API统一响应格式 + * @author DevTTL + * @date 2025/03/10 + */ +@Serializable +data class ApiResponse(val code: Int, val msg: String, val data: T) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt new file mode 100644 index 0000000..903ffed --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt @@ -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.toJson(): String { + val serializer: KSerializer = FlattenBaseReqSerializer( + valueSerializer = this::class.serializer() as KSerializer, + baseSelector = { it.baseReq } + ) + return JsonUtils.default.encodeToString(serializer, this) +} + +fun defaultBaseReq(): BaseReq = BaseReq( + appName = "", + versionCode = 0, + appId = 0, + uniqueId = "", + deviceInfo = "", + deviceVersion = "", + language = "" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/FeedbackType.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/FeedbackType.kt index 85d1580..a13906e 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/FeedbackType.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/FeedbackType.kt @@ -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) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/constant/PointEvent.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/constant/PointEvent.kt new file mode 100644 index 0000000..dbf499b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/constant/PointEvent.kt @@ -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 = mapOf( + "eventName" to eventName, + "eventCode" to eventCode, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt new file mode 100644 index 0000000..beef5b8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt @@ -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" +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt new file mode 100644 index 0000000..ed9c290 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt @@ -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 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 handleResponse(response: HttpResponse): T { + // 使用统一响应处理 + val apiResponse = response.body>() + if (apiResponse.code != 200) { + throw Exception(apiResponse.msg) + } + return apiResponse.data + } +} + +expect val defaultLogger: Logger \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/routes/MainNav.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/routes/MainNav.kt index cc0822a..9ad3b9c 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/routes/MainNav.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/routes/MainNav.kt @@ -176,10 +176,7 @@ fun MainNav() { } // 反馈页面 composable { - FeedbackScreen( - onNavigateBack = { mainNavController.popBackStack() }, - onSubmit = {} - ) + FeedbackScreen(onNavigateBack = { mainNavController.popBackStack() }) } // 隐私 composable { diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/ErrorDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/ErrorDialog.kt new file mode 100644 index 0000000..dae6ef5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/ErrorDialog.kt @@ -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)) + } + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/LoadingScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/LoadingScreen.kt new file mode 100644 index 0000000..0090c4b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/LoadingScreen.kt @@ -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() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/DeviceUtils.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/DeviceUtils.kt new file mode 100644 index 0000000..e7f2124 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/DeviceUtils.kt @@ -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 + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/JsonUtils.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/JsonUtils.kt new file mode 100644 index 0000000..31105ac --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/JsonUtils.kt @@ -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 + } +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/LogUtils.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/LogUtils.kt index 7be9011..4a9c1c5 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/LogUtils.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/LogUtils.kt @@ -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 */ diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/ToastUtils.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/ToastUtils.kt index 9eaf094..bccea55 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/ToastUtils.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/ToastUtils.kt @@ -1,5 +1,10 @@ package com.taskttl.core.utils +/** + * 吐司工具类 + * @author DevTTL + * @date 2025/10/11 + */ expect object ToastUtils { fun show(message: String) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/di/KoinModels.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/di/KoinModels.kt index 3c7272b..e332bee 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/di/KoinModels.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/di/KoinModels.kt @@ -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) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/network/TaskTTLApi.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/TaskTTLApi.kt new file mode 100644 index 0000000..c008a0c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/TaskTTLApi.kt @@ -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) { + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/FeedbackReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/FeedbackReq.kt new file mode 100644 index 0000000..6971b2b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/FeedbackReq.kt @@ -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() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/PointReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/PointReq.kt new file mode 100644 index 0000000..1ba9455 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/req/PointReq.kt @@ -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() + + diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/resp/FeedbackResp.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/resp/FeedbackResp.kt new file mode 100644 index 0000000..db96198 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/network/domain/resp/FeedbackResp.kt @@ -0,0 +1,11 @@ +package com.taskttl.data.network.domain.resp + +import kotlinx.serialization.Serializable + +/** + * 反馈响应 + * @author admin + * @date 2025/10/09 + */ +@Serializable +object FeedbackResp 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 934807a..ec9b133 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CountdownState.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/CountdownState.kt @@ -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() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt new file mode 100644 index 0000000..ac4ff40 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/state/FeedbackState.kt @@ -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() +} \ No newline at end of file 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 f3367f3..8b0281a 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CategoryViewModel.kt @@ -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) + ) } } } 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 8e22037..71d6a67 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CountdownViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/CountdownViewModel.kt @@ -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)) } } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt new file mode 100644 index 0000000..f016b5e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/FeedbackViewModel.kt @@ -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 = _state.asStateFlow() + + private val _effects = MutableSharedFlow() + val effects: SharedFlow = _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) + } + +} \ 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 97d2bad..45e0923 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SplashViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/SplashViewModel.kt @@ -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 } } } - - 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 6480e35..adce2eb 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/TaskViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/viewmodel/TaskViewModel.kt @@ -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) + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryEditorScreen.kt index 4a419d9..5693d72 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryEditorScreen.kt @@ -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() } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryScreen.kt index 46bba8b..ffd5793 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/category/CategoryScreen.kt @@ -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 ) } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownDetailScreen.kt index 9ebd356..94fd294 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownDetailScreen.kt @@ -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() } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownEditorScreen.kt index 94bc783..66ec587 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownEditorScreen.kt @@ -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() } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownScreen.kt index f148a3b..079bb70 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/countdown/CountdownScreen.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/AboutScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/AboutScreen.kt index a9a33c3..0f087b5 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/AboutScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/AboutScreen.kt @@ -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) } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/DataManagementScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/DataManagementScreen.kt index 595c7b6..1fb8de8 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/DataManagementScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/DataManagementScreen.kt @@ -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 ) } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/FeedbackScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/FeedbackScreen.kt index 49788a6..84e1abb 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/FeedbackScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/FeedbackScreen.kt @@ -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)) +} diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/PrivacyScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/PrivacyScreen.kt index e61f644..33b0ff7 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/PrivacyScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/settings/PrivacyScreen.kt @@ -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(), 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 36c457d..e6eb763 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskDetailScreen.kt index 6de4297..606dfca 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskDetailScreen.kt @@ -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() } } diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskEditorScreen.kt index a72e3a1..e2b42a7 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskEditorScreen.kt @@ -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() } } 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 5cac69b..cd0b7ea 100644 --- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/task/TaskScreen.kt @@ -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() } } diff --git a/composeApp/src/iosMain/kotlin/com/taskttl/core/network/KtorClient.ios.kt b/composeApp/src/iosMain/kotlin/com/taskttl/core/network/KtorClient.ios.kt new file mode 100644 index 0000000..ea69f7e --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/taskttl/core/network/KtorClient.ios.kt @@ -0,0 +1,10 @@ +package com.taskttl.core.network + +import com.taskttl.core.utils.LogUtils +import io.ktor.client.plugins.logging.Logger + +actual val defaultLogger: Logger = object : Logger { + override fun log(message: String) { + LogUtils.e("DevTTL_NetWork", message) + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/com/taskttl/core/utils/LogUtils.ios.kt b/composeApp/src/iosMain/kotlin/com/taskttl/core/utils/LogUtils.ios.kt index deaf1ec..43458c9 100644 --- a/composeApp/src/iosMain/kotlin/com/taskttl/core/utils/LogUtils.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/taskttl/core/utils/LogUtils.ios.kt @@ -1,5 +1,6 @@ package com.taskttl.core.utils +import io.ktor.client.plugins.logging.Logger import platform.Foundation.NSLog actual object LogUtils { @@ -18,4 +19,4 @@ actual object LogUtils { actual fun e(tag: String, message: String, throwable: Throwable?) { NSLog("ERROR [$tag]: $message ${throwable?.message ?: ""}") } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 4d047bd..0021f9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,6 @@ kotlin.native.ignoreDisabledTargets=true ksp.verbose=true kotlin.kmp.eagerUnresolvedDependenciesDiagnostic=false -kotlin.kmp.unresolvedDependenciesDiagnostic=false \ No newline at end of file +kotlin.kmp.unresolvedDependenciesDiagnostic=false + +android.overridePathCheck=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82d25cb..eafaac5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidx-core = "1.17.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.4" androidx-testExt = "1.3.0" -composeHotReload = "1.0.0-beta09" +composeHotReload = "1.0.0-rc02" composeMultiplatform = "1.9.0" junit = "4.13.2" kotlin = "2.2.20" @@ -15,27 +15,31 @@ kotlinx-coroutines = "1.10.2" navigationCompose = "2.9.0" koin = "4.1.1" - +ktor = "3.3.1" coil3 = "3.3.0" kotlinx-datetime = "0.7.1" icons = "1.7.3" -google = "4.4.3" -firebase = "34.3.0" +google = "4.4.4" +firebase = "34.4.0" facebookAndroidSdkVersion = "18.1.3" - +playServicesAds = "18.2.0" kotlinx-serialization = "1.9.0" mmkv = "2.2.4" sqlite = "2.6.1" -room = "2.8.1" +room = "2.8.2" ksp = "2.2.20-2.0.2" # 环境 android-compileSdk = "36" android-minSdk = "24" android-targetSdk = "36" + +android-versionCode = "100000" +android-versionName = "1.0.0" + android-facebookAppId = "1203530117944408" android-facebookClientToken = "1ee2da9430c1a589e8aa623bfaaaa586" @@ -63,9 +67,18 @@ koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +# ktor 网络请求 +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } +#implementation("io.ktor:ktor-client-cio:2.3.12") # 桌面 +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor"} + # coil3 coil3-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" } -coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3"} +coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" } coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" } coil3-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3" } @@ -87,7 +100,7 @@ android-facebook-android-sdk = { module = "com.facebook.android:facebook-android kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } # 安卓MMKV -android-mmkv = { module = "com.tencent:mmkv" ,version.ref = "mmkv"} +android-mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } # Room数据库 androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } @@ -95,6 +108,8 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" } +# 谷歌Ads +android-play-services-ads-identifier = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -103,7 +118,7 @@ composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "com composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"} -gms-google = {id = "com.google.gms.google-services", version.ref = "google"} +jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +gms-google = { id = "com.google.gms.google-services", version.ref = "google" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } androidx-room = { id = "androidx.room", version.ref = "room" } \ No newline at end of file