更新
This commit is contained in:
@@ -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 {
|
||||
|
@@ -1,10 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- 添加网络权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- 添加广告ID权限 -->
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||
tools:ignore="AdvertisingIdPolicy" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@@ -13,6 +15,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<activity
|
||||
android:exported="true"
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package com.taskttl.core.network
|
||||
|
||||
import com.taskttl.BuildConfig
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
|
||||
actual val defaultLogger: Logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
LogUtils.e("DevTTL_NetWork", message)
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.btn_retry
|
||||
import taskttl.composeapp.generated.resources.retry
|
||||
import taskttl.composeapp.generated.resources.webview_loading_error
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -157,7 +157,7 @@ actual fun DevTTLWebView(modifier: Modifier, url: String) {
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.btn_retry),
|
||||
text = stringResource(Res.string.retry),
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
|
@@ -0,0 +1,139 @@
|
||||
package com.taskttl.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
||||
import com.taskttl.BuildConfig
|
||||
import com.taskttl.MainApplication
|
||||
import com.taskttl.core.domain.BaseReq
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
|
||||
actual object DeviceUtils {
|
||||
|
||||
private val appContext: Context
|
||||
get() = MainApplication.instance.applicationContext
|
||||
|
||||
private const val PREF_UNIQUE_ID = "PREF_UNIQUE_ID"
|
||||
|
||||
// 请求超时时间
|
||||
private const val REQUEST_TIMEOUT_MS = 5_000L
|
||||
|
||||
actual suspend fun getUniqueId(): String {
|
||||
// 首先检查是否已经生成过唯一ID
|
||||
val savedId = StorageUtils.getString(PREF_UNIQUE_ID)
|
||||
|
||||
// 如果已有保存的ID,直接返回
|
||||
if (savedId.isNotBlank()) {
|
||||
return savedId
|
||||
}
|
||||
|
||||
// 检查网络状态,如果没有网络直接生成本地ID
|
||||
// if (!isNetworkAvailable()) {
|
||||
// val localId = generateAppInstanceId()
|
||||
// MMKVUtils.saveString(PREF_UNIQUE_ID, localId)
|
||||
// return localId
|
||||
// }
|
||||
|
||||
// 尝试获取Google广告ID
|
||||
val adId = try {
|
||||
getGoogleAdId()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val uniqueId = if (isValidAdId(adId)) {
|
||||
adId.toString()
|
||||
} else {
|
||||
// 如果广告ID无效,生成应用实例ID
|
||||
generateAppInstanceId()
|
||||
}
|
||||
|
||||
// 保存生成的ID
|
||||
StorageUtils.saveString(PREF_UNIQUE_ID, uniqueId)
|
||||
return uniqueId
|
||||
}
|
||||
|
||||
actual suspend fun getDeviceInfo(): BaseReq {
|
||||
return BaseReq(
|
||||
appName = BuildConfig.APP_NAME,
|
||||
versionCode = BuildConfig.VERSION_CODE,
|
||||
appId = BuildConfig.APP_ID,
|
||||
uniqueId = getUniqueId(),
|
||||
deviceInfo = Build.MODEL ?: "0",
|
||||
deviceVersion = Build.VERSION.RELEASE,
|
||||
language = ""
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("AdvertisingIdPolicy")
|
||||
private suspend fun getGoogleAdId(): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
withTimeout(REQUEST_TIMEOUT_MS) {
|
||||
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(appContext)
|
||||
if (adInfo.isLimitAdTrackingEnabled) null else adInfo.id
|
||||
}
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否是有效广告id
|
||||
* @param [adId] 广告id
|
||||
* @return [Boolean]
|
||||
*/
|
||||
private fun isValidAdId(adId: String?): Boolean {
|
||||
return !adId.isNullOrBlank() && !adId.matches(Regex("^0+$")) && adId != "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
|
||||
private fun generateAppInstanceId(): String {
|
||||
val deviceIdentifiers = mutableListOf<String>()
|
||||
// 设备硬件信息
|
||||
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()
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">10.0.0.5</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
@@ -1,27 +1,38 @@
|
||||
<resources>
|
||||
<!-- Onboarding -->
|
||||
<string name="continue_text">继续</string>
|
||||
<string name="skip_text">跳过</string>
|
||||
<string name="get_start_text">开始使用</string>
|
||||
|
||||
<string name="onboarding_welcome_title">欢迎使用 TaskTTL</string>
|
||||
<string name="onboarding_welcome_desc">一个简洁而强大的任务管理工具,帮助您高效管理日常任务和重要日期</string>
|
||||
<string name="onboarding_smart_title">智能任务管理</string>
|
||||
<string name="onboarding_smart_desc">创建、分类和跟踪您的任务。设置优先级,添加截止日期,让工作更有条理</string>
|
||||
<string name="onboarding_dates_title">重要日期提醒</string>
|
||||
<string name="onboarding_dates_desc">设置重要日期的倒数计时,永远不会错过生日、纪念日或重要的截止日期</string>
|
||||
<string name="onboarding_ready_title">准备就绪!</string>
|
||||
<string name="onboarding_ready_desc">现在您可以开始创建第一个任务,让我们一起提高工作效率吧!</string>
|
||||
|
||||
<!-- 应用信息 -->
|
||||
<string name="app_name">TaskTTL</string>
|
||||
<string name="app_name_description">任务管理与倒数日应用</string>
|
||||
<string name="app_name_remark">让每一天都更有意义</string>
|
||||
<string name="version">版本</string>
|
||||
<string name="build_version">构建版本</string>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<string name="nav_todo">待办</string>
|
||||
<string name="nav_countdown">倒数日</string>
|
||||
<string name="nav_statistics">统计</string>
|
||||
<string name="nav_settings">设置</string>
|
||||
|
||||
<!-- 任务模块 -->
|
||||
<string name="search_placeholder">搜索任务...</string>
|
||||
<string name="title_task">我的任务</string>
|
||||
<string name="title_task_info">任务详情</string>
|
||||
<string name="title_add_task">添加任务</string>
|
||||
<string name="title_edit_task">编辑任务</string>
|
||||
|
||||
<string name="title_countdown">倒数日</string>
|
||||
<string name="title_countdown_info">倒数日详情</string>
|
||||
<string name="title_add_countdown">添加倒数日</string>
|
||||
<string name="title_edit_countdown">编辑倒数日</string>
|
||||
|
||||
<string name="title_statistics">统计</string>
|
||||
|
||||
<string name="title_about">关于</string>
|
||||
|
||||
<string name="title_category">分类管理</string>
|
||||
<string name="title_add_category">添加分类</string>
|
||||
<string name="title_edit_category">编辑分类</string>
|
||||
|
||||
<string name="label_task_list">任务列表</string>
|
||||
<string name="label_show_completed">显示已完成</string>
|
||||
<string name="text_no_tasks">暂无任务</string>
|
||||
@@ -44,6 +55,23 @@
|
||||
<string name="label_created_at">创建时间:</string>
|
||||
<string name="label_description">任务描述</string>
|
||||
|
||||
<string name="task_add_success">任务添加成功</string>
|
||||
<string name="task_add_failed">添加任务失败</string>
|
||||
<string name="task_update_success">任务更新成功</string>
|
||||
<string name="task_update_failed">更新任务失败</string>
|
||||
<string name="task_delete_success">任务删除成功</string>
|
||||
<string name="task_delete_failed">删除任务失败</string>
|
||||
<string name="task_load_failed">加载任务失败</string>
|
||||
<string name="task_query_failed">查询任务失败</string>
|
||||
<string name="task_status_update_success">更新任务状态成功</string>
|
||||
<string name="task_status_update_failed">更新任务状态失败</string>
|
||||
|
||||
<!-- 倒数日模块 -->
|
||||
<string name="title_countdown">倒数日</string>
|
||||
<string name="title_countdown_info">倒数日详情</string>
|
||||
<string name="title_add_countdown">添加倒数日</string>
|
||||
<string name="title_edit_countdown">编辑倒数日</string>
|
||||
|
||||
<string name="label_countdown_list">倒数日列表</string>
|
||||
<string name="label_days">天</string>
|
||||
<string name="text_no_countdowns">暂无倒数日</string>
|
||||
@@ -52,46 +80,86 @@
|
||||
|
||||
<string name="label_countdown_title">倒数日标题</string>
|
||||
<string name="label_countdown_description">倒数日描述</string>
|
||||
<string name="label_select_category">选择分类</string>
|
||||
<string name="label_target_date">目标日期</string>
|
||||
<string name="label_notification_setting">通知设置</string>
|
||||
<string name="countdown_not_found">倒数日不存在</string>
|
||||
<string name="event_description">事件描述</string>
|
||||
<string name="detail_information">详细信息</string>
|
||||
<string name="reminder">提醒</string>
|
||||
|
||||
<string name="countdown_add_success">倒数日添加成功</string>
|
||||
<string name="countdown_add_failed">添加倒数日失败</string>
|
||||
<string name="countdown_update_success">倒数日更新成功</string>
|
||||
<string name="countdown_update_failed">更新倒数日失败</string>
|
||||
<string name="countdown_delete_success">倒数日删除成功</string>
|
||||
<string name="countdown_delete_failed">删除倒数日失败</string>
|
||||
<string name="countdown_load_failed">加载倒数日失败</string>
|
||||
<string name="countdown_query_failed">查询倒数日失败</string>
|
||||
|
||||
<!-- 统计模块 -->
|
||||
<string name="title_statistics">统计</string>
|
||||
<string name="overview">总览</string>
|
||||
<string name="category_statistics">分类统计</string>
|
||||
<string name="total_tasks">总任务</string>
|
||||
<string name="completed">已完成</string>
|
||||
<string name="completion_rate">完成率</string>
|
||||
<string name="total_countdowns">倒数日总数</string>
|
||||
<string name="active">活跃中</string>
|
||||
|
||||
<string name="overview">总览</string>
|
||||
<string name="category_statistics">分类统计</string>
|
||||
<!-- 分类模块 -->
|
||||
<string name="category_task">任务</string>
|
||||
<string name="category_countdown">倒数日</string>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<string name="countdown_not_found">倒数日不存在</string>
|
||||
<string name="title_category">分类管理</string>
|
||||
<string name="title_add_category">添加分类</string>
|
||||
<string name="title_edit_category">编辑分类</string>
|
||||
<string name="label_no_category">暂无分类</string>
|
||||
<string name="label_add_category_hint">点击右下角按钮添加新分类</string>
|
||||
<string name="label_category_name">分类名称</string>
|
||||
<string name="placeholder_category_name">输入分类名称...</string>
|
||||
<string name="label_category_type">分类类型</string>
|
||||
<string name="label_select_color">选择颜色</string>
|
||||
<string name="label_select_icon">选择图标</string>
|
||||
<string name="label_task_category">任务分类</string>
|
||||
<string name="label_countdown_category">倒数日分类</string>
|
||||
|
||||
<!-- 倒数日卡片 -->
|
||||
<string name="unit_days">天</string>
|
||||
<string name="event_description">事件描述</string>
|
||||
<string name="detail_information">详细信息</string>
|
||||
<string name="label_task_count">%1$d 个任务</string>
|
||||
<string name="label_countdown_count">%1$d 个倒数日</string>
|
||||
|
||||
<!-- 信息项 -->
|
||||
<string name="reminder">提醒</string>
|
||||
<string name="created_at">创建时间</string>
|
||||
<!-- 分类操作反馈 -->
|
||||
<string name="category_add_success">分类添加成功</string>
|
||||
<string name="category_add_failed">添加分类失败</string>
|
||||
<string name="category_update_success">分类更新成功</string>
|
||||
<string name="category_update_failed">更新分类失败</string>
|
||||
<string name="category_delete_success">分类删除成功</string>
|
||||
<string name="category_delete_failed">删除分类失败</string>
|
||||
<string name="category_load_failed">加载分类失败</string>
|
||||
<string name="category_stat_failed">加载统计数据失败</string>
|
||||
<string name="category_init_success">默认分类初始化成功</string>
|
||||
<string name="category_init_failed">初始化默认分类失败</string>
|
||||
<string name="category_not_found">查询分类失败</string>
|
||||
<string name="category_count_update_failed">更新分类计数失败</string>
|
||||
|
||||
<!-- 通用 -->
|
||||
<!-- 通用操作 -->
|
||||
<string name="enter">进入</string>
|
||||
<string name="edit">编辑</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="confirm">确定</string>
|
||||
<string name="delete">删除</string>
|
||||
<string name="export">导出</string>
|
||||
<string name="import">导入</string>
|
||||
<string name="choose_file">选择文件</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="action">操作</string>
|
||||
<string name="all_text">全部</string>
|
||||
<string name="search_placeholder">搜索任务...</string>
|
||||
<string name="search">搜索</string>
|
||||
<string name="clear_text">清除</string>
|
||||
<string name="all_text">全部</string>
|
||||
<string name="retry">重试</string>
|
||||
<string name="choose_file">选择文件</string>
|
||||
<string name="error_title">错误</string>
|
||||
<string name="webview_loading_error">加载失败,请检查网络连接</string>
|
||||
|
||||
<!-- DataManagement 模块 -->
|
||||
|
||||
<!-- 数据管理 -->
|
||||
<string name="title_data_management">数据管理</string>
|
||||
<string name="title_export_data">导出数据</string>
|
||||
<string name="desc_export_data">将所有任务和倒数日导出为文件</string>
|
||||
@@ -101,45 +169,27 @@
|
||||
<string name="desc_auto_backup">定期自动备份数据到云端</string>
|
||||
<string name="title_clear_all_data">清除所有数据</string>
|
||||
<string name="desc_clear_all_data">删除所有任务、倒数日和设置</string>
|
||||
<string name="title_clear_completed_tasks">清理已完成任务</string>
|
||||
<string name="desc_clear_completed_tasks">删除所有已完成的任务</string>
|
||||
<string name="title_clear_expired_countdowns">清理过期倒数日</string>
|
||||
<string name="desc_clear_expired_countdowns">删除所有已过期的倒数日</string>
|
||||
<string name="desc_clear_all_data_dialog">此操作将删除所有任务、倒数日和设置,且无法恢复。</string>
|
||||
<string name="title_clear_completed_tasks">清理已完成任务</string>
|
||||
<string name="title_clear_expired_countdowns">清理过期倒数日</string>
|
||||
<string name="desc_clear_completed_tasks">删除所有已完成的任务</string>
|
||||
<string name="desc_clear_expired_countdowns">删除所有已过期的倒数日</string>
|
||||
|
||||
<string name="label_select_import_file">选择要导入的文件</string>
|
||||
<string name="label_enter">进入</string>
|
||||
<string name="title_data_clean">数据清理</string>
|
||||
<string name="title_backup_restore">备份与恢复</string>
|
||||
|
||||
<!-- 对话框 / 文件选择 -->
|
||||
<string name="title_data_clean">数据清理</string>
|
||||
<string name="label_select_import_file">选择要导入的文件</string>
|
||||
<string name="label_select_file">选择文件</string>
|
||||
<string name="label_select_export_format">选择导出格式</string>
|
||||
<string name="label_json_format">JSON格式</string>
|
||||
<string name="label_csv_format">CSV格式</string>
|
||||
|
||||
<string name="label_no_category">暂无分类</string>
|
||||
<string name="label_add_category_hint">点击右下角按钮添加新分类</string>
|
||||
<string name="label_edit">编辑</string>
|
||||
<string name="label_task_count">%1$d 个任务</string>
|
||||
<string name="label_countdown_count">%1$d 个倒数日</string>
|
||||
|
||||
<string name="label_category_name">分类名字</string>
|
||||
<string name="placeholder_category_name">输入分类名称...</string>
|
||||
<string name="label_category_type">分类类型</string>
|
||||
<string name="label_select_color">选择颜色</string>
|
||||
<string name="label_select_icon">选择图标</string>
|
||||
<string name="label_task_category">任务分类</string>
|
||||
<string name="label_countdown_category">倒数日分类</string>
|
||||
|
||||
<!-- 标题 -->
|
||||
<!-- 设置模块 -->
|
||||
<string name="title_app_settings">应用设置</string>
|
||||
<string name="section_general_settings">通用设置</string>
|
||||
<string name="section_data_management">数据管理</string>
|
||||
<string name="section_social_share">社交分享</string>
|
||||
<string name="section_help_feedback">帮助与反馈</string>
|
||||
|
||||
<!-- 通用设置 -->
|
||||
<string name="setting_push_notification">推送通知</string>
|
||||
<string name="setting_push_notification_desc">接收任务和倒数日提醒</string>
|
||||
<string name="setting_dark_mode">深色模式</string>
|
||||
@@ -147,19 +197,16 @@
|
||||
<string name="setting_language">语言设置</string>
|
||||
<string name="setting_language_desc">简体中文</string>
|
||||
|
||||
<!-- 数据管理 -->
|
||||
<string name="setting_category_management">分类管理</string>
|
||||
<string name="setting_category_management_desc">管理分类</string>
|
||||
<string name="setting_data_management">数据管理</string>
|
||||
<string name="setting_data_management_desc">备份和恢复数据</string>
|
||||
|
||||
<!-- 社交分享 -->
|
||||
<string name="setting_share_achievement">分享成就</string>
|
||||
<string name="setting_share_achievement_desc">分享任务完成成就</string>
|
||||
<string name="setting_invite_friend">推荐给朋友</string>
|
||||
<string name="setting_invite_friend_desc">邀请朋友使用 TaskMaster</string>
|
||||
|
||||
<!-- 帮助与反馈 -->
|
||||
<string name="setting_feedback">意见反馈</string>
|
||||
<string name="setting_feedback_desc">告诉我们您的想法</string>
|
||||
<string name="setting_privacy_policy">隐私政策</string>
|
||||
@@ -167,95 +214,47 @@
|
||||
<string name="setting_about_app">关于应用</string>
|
||||
<string name="setting_about_app_desc">版本 1.0.0</string>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<string name="user_name">TaskMaster 用户</string>
|
||||
<string name="user_info">已使用 %1$d 天 · 完成 %2$d 个任务</string>
|
||||
<!-- 反馈与帮助 -->
|
||||
<string name="title_feedback">意见反馈</string>
|
||||
<string name="feedback_type">反馈类型</string>
|
||||
<string name="feedback_issue">问题反馈</string>
|
||||
<string name="feedback_suggestion">功能建议</string>
|
||||
<string name="feedback_description">问题描述</string>
|
||||
<string name="feedback_placeholder">请详细描述您遇到的问题或建议...</string>
|
||||
<string name="feedback_contact">联系方式(可选)</string>
|
||||
<string name="feedback_contact_placeholder">您的邮箱地址,方便我们回复</string>
|
||||
<string name="feedback_submitted">感谢您的反馈!我们会尽快处理。</string>
|
||||
<string name="feedback_error_empty">请填写反馈内容</string>
|
||||
<string name="button_send_feedback">发送反馈</string>
|
||||
|
||||
<!-- 其他 -->
|
||||
<string name="category_name_placeholder">输入分类名称...</string>
|
||||
|
||||
|
||||
<!-- 版本信息 -->
|
||||
<string name="version">版本</string>
|
||||
<string name="build_version">构建版本</string>
|
||||
|
||||
<!-- 应用介绍 -->
|
||||
<!-- 关于页面 -->
|
||||
<string name="title_about">关于</string>
|
||||
<string name="app_intro_title">应用介绍</string>
|
||||
<string name="app_intro_content">
|
||||
TaskTTL 是一款现代化的任务管理与倒数日应用,帮助您高效管理日常任务与重要日期。
|
||||
支持分类管理、优先级设置与统计分析,让生活更有条理。
|
||||
TaskTTL 是一款现代化的任务管理与倒数日应用,
|
||||
支持分类管理、优先级设置与统计分析,让生活更有条理。
|
||||
</string>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<string name="title_privacy">隐私协议</string>
|
||||
|
||||
<string name="tech_stack">技术栈</string>
|
||||
<string name="tech_stack_kmp">Kotlin Multiplatform(跨平台开发框架)</string>
|
||||
<string name="tech_stack_compose">Jetpack Compose(现代化 UI 框架)</string>
|
||||
<string name="tech_stack_room">Room Database(本地存储)</string>
|
||||
<string name="tech_stack_koin">Koin(依赖注入框架)</string>
|
||||
<string name="tech_stack_ktor">Ktor(网络请求)</string>
|
||||
<string name="tech_stack_mvi">MVI Architecture(响应式架构模式)</string>
|
||||
|
||||
<!-- 开发者信息 -->
|
||||
<string name="developer_text">开发者</string>
|
||||
<string name="devttl_team">DevTTL 团队</string>
|
||||
|
||||
<!-- 联系我们 -->
|
||||
<string name="contact_us">联系我们</string>
|
||||
<string name="email_text">电子邮箱</string>
|
||||
<string name="email">team@devttl.com</string>
|
||||
<string name="email">admin@devttl.com</string>
|
||||
<string name="web_text">官方网站</string>
|
||||
<string name="web_url">https://devttl.com</string>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<string name="copyright_year">2025</string>
|
||||
<string name="all_rights_reserved">保留所有权利</string>
|
||||
|
||||
<string name="button_send_feedback">发送反馈</string>
|
||||
<string name="button_cancel">取消</string>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<string name="title_feedback">意见反馈</string>
|
||||
|
||||
<!-- 反馈类型 -->
|
||||
<string name="feedback_type">反馈类型</string>
|
||||
<string name="feedback_issue">问题反馈</string>
|
||||
<string name="feedback_suggestion">功能建议</string>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<string name="feedback_description">问题描述</string>
|
||||
<string name="feedback_placeholder">请详细描述您遇到的问题或建议...</string>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<string name="feedback_contact">联系方式(可选)</string>
|
||||
<string name="feedback_contact_placeholder">您的邮箱地址,方便我们回复</string>
|
||||
|
||||
<!-- 提交提示 -->
|
||||
<string name="feedback_submitted">感谢您的反馈!我们会尽快处理。</string>
|
||||
<string name="feedback_error_empty">请填写反馈内容</string>
|
||||
|
||||
|
||||
<string name="app_name">TaskMaster</string>
|
||||
<string name="app_name_description">任务管理与倒数日应用</string>
|
||||
<string name="app_name_remark">让每一天都更有意义</string>
|
||||
|
||||
<string name="continue_text">继续</string>
|
||||
<string name="skip_text">跳过</string>
|
||||
<string name="get_start_text">开始使用</string>
|
||||
|
||||
<string name="onboarding_welcome_title">欢迎使用 TaskMaster</string>
|
||||
<string name="onboarding_welcome_desc">一个简洁而强大的任务管理工具,帮助您高效管理日常任务和重要日期</string>
|
||||
|
||||
<string name="onboarding_smart_title">智能任务管理</string>
|
||||
<string name="onboarding_smart_desc">创建、分类和跟踪您的任务。设置优先级,添加截止日期,让工作更有条理</string>
|
||||
|
||||
<string name="onboarding_dates_title">重要日期提醒</string>
|
||||
<string name="onboarding_dates_desc">设置重要日期的倒数计时,永远不会错过生日、纪念日或重要的截止日期</string>
|
||||
|
||||
<string name="onboarding_ready_title">准备就绪!</string>
|
||||
<string name="onboarding_ready_desc">现在您可以开始创建第一个任务,让我们一起提高工作效率吧!</string>
|
||||
|
||||
<string name="category_task">任务</string>
|
||||
<string name="category_countdown">倒数日</string>
|
||||
|
||||
<string name="priority_low">低</string>
|
||||
<string name="priority_medium">中</string>
|
||||
<string name="priority_high">高</string>
|
||||
@@ -306,8 +305,9 @@
|
||||
<string name="category_goal">目标</string>
|
||||
<string name="category_reminder">提醒</string>
|
||||
|
||||
<string name="webview_loading_error">加载失败,请检查网络连接</string>
|
||||
<string name="btn_retry">重试</string>
|
||||
<string name="privacy_url">https://devttl.com</string>
|
||||
<string name="privacy_url">https://sites.google.com/view/taskttl/privacy</string>
|
||||
|
||||
</resources>
|
||||
<string name="feedback_success">反馈成功</string>
|
||||
<string name="feedback_error">反馈失败,请检查网络连接或稍后重试</string>
|
||||
|
||||
</resources>
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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 ?: ""}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,4 +17,6 @@ kotlin.native.ignoreDisabledTargets=true
|
||||
ksp.verbose=true
|
||||
|
||||
kotlin.kmp.eagerUnresolvedDependenciesDiagnostic=false
|
||||
kotlin.kmp.unresolvedDependenciesDiagnostic=false
|
||||
kotlin.kmp.unresolvedDependenciesDiagnostic=false
|
||||
|
||||
android.overridePathCheck=true
|
@@ -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" }
|
Reference in New Issue
Block a user