This commit is contained in:
2025-10-19 21:51:36 +08:00
parent c7d738ccce
commit 5f07605a06
22 changed files with 447 additions and 67 deletions

View File

@@ -62,7 +62,7 @@ kotlin {
implementation(project.dependencies.platform(libs.firebase.bom)) implementation(project.dependencies.platform(libs.firebase.bom))
implementation(libs.firebase.analytics) implementation(libs.firebase.analytics)
// facebook // facebook
// implementation(libs.android.facebook.android.sdk) implementation(libs.android.facebook.android.sdk)
// mmkv // mmkv
implementation(libs.android.mmkv) implementation(libs.android.mmkv)
@@ -72,6 +72,9 @@ kotlin {
// admob // admob
implementation(libs.android.play.services.ads.identifier) implementation(libs.android.play.services.ads.identifier)
// work
implementation(libs.androidx.work)
} }
commonMain.dependencies { commonMain.dependencies {
implementation(compose.runtime) implementation(compose.runtime)

View File

@@ -1,12 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:tools="http://schemas.android.com/tools">
<!-- 添加网络权限 --> <!-- 添加网络权限 -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- 添加广告ID权限 --> <!-- 添加广告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" /> tools:ignore="AdvertisingIdPolicy" />
<!--通知权限-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--Android 12+ 精确闹钟-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -17,9 +23,9 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity <activity
android:exported="true" android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:name=".MainActivity"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -39,6 +45,50 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.facebook.FacebookActivity"
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" />
<activity
android:name="com.facebook.CustomTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="fb${facebookAppId}" />
</intent-filter>
</activity>
<!-- AlarmReceiver -->
<receiver
android:name=".core.alarm.AlarmReceiver"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.taskttl.ALARM_TRIGGER" />
</intent-filter>
</receiver>
<receiver
android:name=".core.alarm.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- Facebook -->
<meta-data
android:name="com.facebook.sdk.ApplicationId"
android:value="${facebookAppId}" />
<meta-data
android:name="com.facebook.sdk.ClientToken"
android:value="${facebookClientToken}" />
</application> </application>
<queries> <queries>

View File

@@ -1,17 +1,34 @@
package com.taskttl package com.taskttl
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
// 设置全屏显示
WindowCompat.setDecorFitsSystemWindows(window, true)
// 设置状态栏图标颜色
val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
// windowInsetsController.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
// windowInsetsController.systemBarsBehavior =
// WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// 使用系统原生方法检测暗色主题
val isDarkTheme = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
windowInsetsController.isAppearanceLightStatusBars = !isDarkTheme
setContent { setContent {
App() App()
} }

View File

@@ -1,5 +1,6 @@
package com.taskttl package com.taskttl
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
@@ -12,15 +13,14 @@ import org.koin.android.ext.koin.androidLogger
class MainApplication : Application() { class MainApplication : Application() {
companion object { companion object {
lateinit var instance: Application @SuppressLint("StaticFieldLeak")
}
@Volatile @Volatile
var currentActivity: Activity? = null private var _currentActivity: Activity? = null
private set
lateinit var instance: Application
fun currentActivity(): Activity? = _currentActivity
}
init { init {
instance = this instance = this
@@ -32,12 +32,12 @@ class MainApplication : Application() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) { override fun onActivityResumed(activity: Activity) {
currentActivity = activity _currentActivity = activity
} }
override fun onActivityPaused(activity: Activity) { override fun onActivityPaused(activity: Activity) {
if (currentActivity == activity) { if (_currentActivity == activity) {
currentActivity = null _currentActivity = null
} }
} }

View File

@@ -0,0 +1,25 @@
package com.taskttl.core.analytics
import android.os.Bundle
import com.facebook.FacebookSdk
import com.facebook.appevents.AppEventsLogger
actual object FacebookEventTracker {
private val logger: AppEventsLogger by lazy {
AppEventsLogger.newLogger(FacebookSdk.getApplicationContext())
}
actual fun logEvent(event: FacebookEvent, params: Map<String, Any?>) {
val bundle = Bundle()
params.forEach { (key, value) ->
when (value) {
is String -> bundle.putString(key, value)
is Double -> bundle.putDouble(key, value)
is Int -> bundle.putInt(key, value)
is Boolean -> bundle.putBoolean(key, value)
}
}
logger.logEvent(event.eventName, bundle)
}
}

View File

@@ -215,9 +215,9 @@
<string name="setting_privacy_rate">应用评价</string> <string name="setting_privacy_rate">应用评价</string>
<string name="setting_privacy_rate_desc">如果喜欢,欢迎在商店留下五星好评</string> <string name="setting_privacy_rate_desc">如果喜欢,欢迎在商店留下五星好评</string>
<string name="setting_about_app">关于应用</string> <string name="setting_about_app">关于应用</string>
<string name="app_version_code">100001</string> <string name="app_version_code">100002</string>
<string name="app_version_name">1.0.1</string> <string name="app_version_name">1.0.2</string>
<string name="setting_about_app_desc">版本 1.0.1</string> <string name="setting_about_app_desc">版本 1.0.2</string>
<!-- 反馈与帮助 --> <!-- 反馈与帮助 -->
<string name="title_feedback">意见反馈</string> <string name="title_feedback">意见反馈</string>

View File

@@ -216,9 +216,9 @@
<string name="setting_privacy_rate">App Review</string> <string name="setting_privacy_rate">App Review</string>
<string name="setting_privacy_rate_desc">If you like it, please leave a five-star review in the store</string> <string name="setting_privacy_rate_desc">If you like it, please leave a five-star review in the store</string>
<string name="setting_about_app">About the App</string> <string name="setting_about_app">About the App</string>
<string name="app_version_code">100001</string> <string name="app_version_code">100002</string>
<string name="app_version_name">1.0.1</string> <string name="app_version_name">1.0.2</string>
<string name="setting_about_app_desc">Version 1.0.1</string> <string name="setting_about_app_desc">Version 1.0.2</string>
<!-- Feedback & Help --> <!-- Feedback & Help -->
<string name="title_feedback">Feedback</string> <string name="title_feedback">Feedback</string>

View File

@@ -0,0 +1,128 @@
package com.taskttl.core.analytics
/**
* Facebook 标准事件定义(官方事件名 + 必填字段)
* https://developers.facebook.com/docs/app-events/reference
*/
sealed class FacebookEvent(
val eventName: String,
val requiredKeys: List<String> = emptyList(),
) {
/**
* 参数
* @return [Map<String, Any?>]
*/
abstract fun params(): Map<String, Any?>
data class Login(
val method: String? = null
) : FacebookEvent("fb_mobile_login", listOf("method")) {
override fun params() = mapOf("method" to method)
}
// 内容类 / 电商类
data class ViewContent(
val contentId: String,
val contentType: String? = null,
val value: Double? = null,
val currency: String? = null
) : FacebookEvent("fb_mobile_content_view", listOf("content_id")) {
override fun params() = mapOf(
"content_id" to contentId,
"content_type" to contentType,
"value" to value,
"currency" to currency
)
}
// 游戏 / 广告类
data class AchieveLevel(val level: Int) :
FacebookEvent("fb_mobile_level_achieved", listOf("level")) {
override fun params() = mapOf("level" to level)
}
/**
* 广告次数
* @author admin
* @date 2025/10/19
* @constructor 创建[AdImpression]
* @param [adPlatform] 广告平台 如 "AdMob", "ironSource", "AppLovin" 等
* @param [adSource] 广告来源或网络,如 "admob_banner", "admob_interstitial"
* @param [adFormat] 广告类型,如 "banner", "interstitial", "rewarded_video"
* @param [adPlacementId] 广告位 ID
* @param [value] 收益金额
* @param [currency] 收益货币ISO 代码,如 "USD"
* @param [placement] 自定义广告位名称
*/
data class AdImpression(
val adPlatform: String,
val adSource: String,
val adFormat: String,
val adPlacementId: String,
val currency: String,
val value: Double,
val placement: String? = null
) : FacebookEvent(
"ad_impression",
listOf("ad_platform", "ad_format", "ad_placement_id", "currency", "value")
) {
override fun params() = mapOf(
"ad_platform" to adPlatform,
"ad_source" to adSource,
"ad_format" to adFormat,
"ad_placement_id" to adPlacementId,
"currency" to currency,
"value" to value,
"placement" to placement
)
}
data class AddToCart(
val contentId: String,
val value: Double,
val currency: String
) : FacebookEvent("fb_mobile_add_to_cart", listOf("content_id", "value", "currency")) {
override fun params() = mapOf(
"content_id" to contentId,
"value" to value,
"currency" to currency
)
}
data class Purchase(
val value: Double,
val currency: String,
val contentId: String? = null
) : FacebookEvent("fb_mobile_purchase", listOf("value", "currency")) {
override fun params() = mapOf(
"value" to value,
"currency" to currency,
"content_id" to contentId
)
}
/**
* 订阅 / 试用类
* @author admin
* @date 2025/10/19
* @constructor 创建[StartTrial]
* @param [trialName] 试验名称
*/
data class StartTrial(val trialName: String? = null) :
FacebookEvent("start_trial") {
override fun params() = mapOf("trial_name" to trialName)
}
/**
* 订阅
* @author admin
* @date 2025/10/19
* @constructor 创建[Subscribe]
* @param [plan] 计划
*/
data class Subscribe(val plan: String? = null) :
FacebookEvent("subscribe") {
override fun params() = mapOf("plan" to plan)
}
}

View File

@@ -0,0 +1,8 @@
package com.taskttl.core.analytics
/**
* 统一事件接口
*/
expect object FacebookEventTracker {
fun logEvent(event: FacebookEvent, params: Map<String, Any?> = emptyMap())
}

View File

@@ -0,0 +1,25 @@
package com.taskttl.core.analytics
/**
* facebook事件验证器
* @author admin
* @date 2025/10/19
*/
object FacebookEventValidator {
/**
* 验证
* @param [event] 事件
* @return [String?]
*/
fun validate(event: FacebookEvent): String? {
val params = event.params()
val missing = event.requiredKeys.filter { key ->
val v = params[key]
v == null || (v is String && v.isBlank())
}
return if (missing.isNotEmpty()) {
"⚠️ Missing required fields for ${event.eventName}: ${missing.joinToString()}"
} else null
}
}

View File

@@ -81,4 +81,12 @@ object DateUtils {
CountdownTime(days, hours, minutes, seconds, isExpired = false) CountdownTime(days, hours, minutes, seconds, isExpired = false)
} }
} }
fun LocalDateTime.toEpochMillis(): Long {
return this.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
}
fun Long.toLocalDateTime(): LocalDateTime {
return Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault())
}
} }

View File

@@ -1,5 +1,6 @@
package com.taskttl.data.di package com.taskttl.data.di
import com.taskttl.core.notification.NotificationManager
import com.taskttl.data.repository.OnboardingRepository import com.taskttl.data.repository.OnboardingRepository
import com.taskttl.data.repository.SettingsRepository import com.taskttl.data.repository.SettingsRepository
import com.taskttl.data.repository.impl.OnboardingRepositoryImpl import com.taskttl.data.repository.impl.OnboardingRepositoryImpl

View File

@@ -1,5 +1,6 @@
package com.taskttl.data.state package com.taskttl.data.state
import com.taskttl.core.notification.NotificationPermissionManager
import com.taskttl.core.viewmodel.BaseUiState import com.taskttl.core.viewmodel.BaseUiState
/** /**
@@ -14,6 +15,7 @@ data class SettingsState(
override val isLoading: Boolean = false, override val isLoading: Boolean = false,
override val isProcessing: Boolean = false, override val isProcessing: Boolean = false,
override val error: String? = null, override val error: String? = null,
val isNotification: Boolean = NotificationPermissionManager.verifyPermission(),
) : BaseUiState() ) : BaseUiState()
/** /**
@@ -38,6 +40,17 @@ sealed class SettingsIntent {
* @param [url] 网址 * @param [url] 网址
*/ */
class OpenUrl(val url: String) : SettingsIntent() class OpenUrl(val url: String) : SettingsIntent()
object RequestPermission : SettingsIntent()
/**
* 更新通知状态
* @author DevTTL
* @date 2025/10/17
* @constructor 创建[UpdateNotificationStatus]
* @param [status] 状态
*/
class UpdateNotificationStatus(val status: Boolean) : SettingsIntent()
} }

View File

@@ -39,6 +39,7 @@ data class TaskState(
* @constructor 创建[TaskIntent] * @constructor 创建[TaskIntent]
*/ */
sealed class TaskIntent { sealed class TaskIntent {
/** /**
* 加载任务 * 加载任务
* @author admin * @author admin

View File

@@ -1,6 +1,10 @@
package com.taskttl.data.viewmodel package com.taskttl.data.viewmodel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.taskttl.core.notification.NotificationManager
import com.taskttl.core.notification.NotificationPayload
import com.taskttl.core.notification.NotificationRepeatType
import com.taskttl.core.utils.DateUtils.toEpochMillis
import com.taskttl.core.viewmodel.BaseViewModel import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType import com.taskttl.data.local.model.CategoryType

View File

@@ -1,7 +1,10 @@
package com.taskttl.data.viewmodel package com.taskttl.data.viewmodel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.taskttl.core.notification.NotificationPermissionCallback
import com.taskttl.core.notification.NotificationPermissionManager
import com.taskttl.core.utils.ExternalAppLauncher import com.taskttl.core.utils.ExternalAppLauncher
import com.taskttl.core.utils.LogUtils
import com.taskttl.core.viewmodel.BaseViewModel import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.state.SettingsEffect import com.taskttl.data.state.SettingsEffect
import com.taskttl.data.state.SettingsIntent import com.taskttl.data.state.SettingsIntent
@@ -22,6 +25,8 @@ class SettingsViewModel() :
when (intent) { when (intent) {
is SettingsIntent.OpenAppRating -> openAppRating() is SettingsIntent.OpenAppRating -> openAppRating()
is SettingsIntent.OpenUrl -> openUrl(intent.url) is SettingsIntent.OpenUrl -> openUrl(intent.url)
is SettingsIntent.RequestPermission -> requestPermission()
is SettingsIntent.UpdateNotificationStatus -> updateNotificationStatus(intent.status)
} }
} }
@@ -37,6 +42,25 @@ class SettingsViewModel() :
} }
} }
private fun requestPermission() {
viewModelScope.launch {
if (NotificationPermissionManager.verifyPermission()) {
NotificationPermissionManager.disablePermission()
} else {
NotificationPermissionManager.requestPermission(
object : NotificationPermissionCallback {
override fun onGranted() = updateState { copy(isNotification = true) }
override fun onDenied() = LogUtils.e("DevTTL", "❌ Android 通知权限被拒绝")
}
)
}
}
}
private fun updateNotificationStatus(status: Boolean) {
updateState { copy(isNotification = status) }
}
/** /**
* 清除错误 * 清除错误
*/ */

View File

@@ -1,7 +1,10 @@
package com.taskttl.data.viewmodel package com.taskttl.data.viewmodel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.taskttl.core.utils.LogUtils import com.taskttl.core.notification.NotificationManager
import com.taskttl.core.notification.NotificationPayload
import com.taskttl.core.notification.NotificationRepeatType
import com.taskttl.core.utils.DateUtils.toEpochMillis
import com.taskttl.core.viewmodel.BaseViewModel import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType import com.taskttl.data.local.model.CategoryType
@@ -24,6 +27,8 @@ import taskttl.composeapp.generated.resources.task_status_update_failed
import taskttl.composeapp.generated.resources.task_status_update_success import taskttl.composeapp.generated.resources.task_status_update_success
import taskttl.composeapp.generated.resources.task_update_failed import taskttl.composeapp.generated.resources.task_update_failed
import taskttl.composeapp.generated.resources.task_update_success import taskttl.composeapp.generated.resources.task_update_success
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
/** /**
* 任务视图模型 * 任务视图模型
@@ -87,10 +92,7 @@ class TaskViewModel(
try { try {
launch { launch {
categoryRepository.getCategoriesByType(CategoryType.TASK) categoryRepository.getCategoriesByType(CategoryType.TASK)
.collect { categories -> .collect { categories -> updateState { copy(categories = categories) } }
LogUtils.e("DevTTL", categories.toString())
updateState { copy(categories = categories) }
}
} }
launch { launch {
taskRepository.getAllTasks().collect { tasks -> taskRepository.getAllTasks().collect { tasks ->
@@ -104,7 +106,6 @@ class TaskViewModel(
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
LogUtils.e("DevTTL", e.message.toString())
val errStr = getString(Res.string.task_load_failed) val errStr = getString(Res.string.task_load_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) } updateState { copy(isLoading = false, error = e.message ?: errStr) }
} }
@@ -131,19 +132,36 @@ class TaskViewModel(
* 添加任务 * 添加任务
* @param [task] 任务 * @param [task] 任务
*/ */
@OptIn(ExperimentalTime::class)
private fun addTask(task: Task) { private fun addTask(task: Task) {
viewModelScope.launch { viewModelScope.launch {
try { try {
if (state.value.isProcessing) return@launch if (state.value.isProcessing) return@launch
updateState { copy(isLoading = false, isProcessing = true) } updateState { copy(isLoading = false, isProcessing = true) }
// 1⃣ 插入任务
taskRepository.insertTask(task) taskRepository.insertTask(task)
// 2⃣ 创建通知
task.dueDate?.let {
NotificationManager.scheduleNotification(
NotificationPayload(
id = task.id,
title = task.title,
message = task.description,
triggerTimeMillis = task.dueDate.toEpochMillis(),
repeatType = NotificationRepeatType.NONE
)
)
}
// 3⃣ UI 提示
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_add_success))) sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_add_success)))
sendEvent(TaskEffect.NavigateBack) sendEvent(TaskEffect.NavigateBack)
} catch (e: Exception) { } catch (e: Exception) {
val errStr = getString(Res.string.task_add_failed) val errStr = getString(Res.string.task_add_failed)
updateState { copy(error = e.message ?: errStr) } updateState { copy(error = e.message ?: errStr) }
} finally { } finally {
updateState { copy(isLoading = false,isProcessing = false) } updateState { copy(isLoading = false, isProcessing = false) }
} }
} }
} }
@@ -158,13 +176,27 @@ class TaskViewModel(
if (state.value.isProcessing) return@launch if (state.value.isProcessing) return@launch
updateState { copy(isLoading = false, isProcessing = true) } updateState { copy(isLoading = false, isProcessing = true) }
taskRepository.updateTask(task) taskRepository.updateTask(task)
NotificationManager.cancelNotification(task.id)
task.dueDate?.let {
NotificationManager.scheduleNotification(
NotificationPayload(
id = task.id,
title = task.title,
message = task.description,
triggerTimeMillis = task.dueDate.toEpochMillis(),
repeatType = NotificationRepeatType.NONE
)
)
}
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_update_success))) sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_update_success)))
sendEvent(TaskEffect.NavigateBack) sendEvent(TaskEffect.NavigateBack)
} catch (e: Exception) { } catch (e: Exception) {
val errStr = getString(Res.string.task_update_failed) val errStr = getString(Res.string.task_update_failed)
updateState { copy(error = e.message ?: errStr) } updateState { copy(error = e.message ?: errStr) }
} finally { } finally {
updateState { copy(isLoading = false,isProcessing = false) } updateState { copy(isLoading = false, isProcessing = false) }
} }
} }
} }
@@ -179,12 +211,13 @@ class TaskViewModel(
if (state.value.isProcessing) return@launch if (state.value.isProcessing) return@launch
updateState { copy(isLoading = false, isProcessing = true) } updateState { copy(isLoading = false, isProcessing = true) }
taskRepository.deleteTask(taskId) taskRepository.deleteTask(taskId)
NotificationManager.cancelNotification(taskId)
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_delete_success))) sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
} catch (e: Exception) { } catch (e: Exception) {
val errStr = getString(Res.string.task_delete_failed) val errStr = getString(Res.string.task_delete_failed)
updateState { copy(error = e.message ?: errStr) } updateState { copy(error = e.message ?: errStr) }
} finally { } finally {
updateState { copy(isLoading = false,isProcessing = false) } updateState { copy(isLoading = false, isProcessing = false) }
} }
} }
} }

View File

@@ -32,7 +32,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.taskttl.core.notification.NotificationPermissionCallback
import com.taskttl.core.notification.NotificationPermissionManager
import com.taskttl.core.routes.Routes import com.taskttl.core.routes.Routes
import com.taskttl.core.utils.LogUtils
import com.taskttl.data.local.model.OnboardingPage import com.taskttl.data.local.model.OnboardingPage
import com.taskttl.data.state.OnboardingEffect import com.taskttl.data.state.OnboardingEffect
import com.taskttl.data.state.OnboardingIntent import com.taskttl.data.state.OnboardingIntent
@@ -63,6 +66,16 @@ fun OnboardingScreen(
val pagerState = val pagerState =
rememberPagerState(0, initialPageOffsetFraction = 0f, pageCount = { onboardingPages.size }) rememberPagerState(0, initialPageOffsetFraction = 0f, pageCount = { onboardingPages.size })
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(300)
NotificationPermissionManager.requestPermission(
object : NotificationPermissionCallback {
override fun onGranted() = LogUtils.e("DevTTL", "✅ Android 通知权限已授予")
override fun onDenied() = LogUtils.e("DevTTL", "❌ Android 通知权限被拒绝")
}
)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.effects.collectLatest { event -> viewModel.effects.collectLatest { event ->
when (event) { when (event) {

View File

@@ -21,17 +21,15 @@ import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -40,8 +38,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.taskttl.core.notification.NotificationPermissionManager
import com.taskttl.core.routes.Routes import com.taskttl.core.routes.Routes
import com.taskttl.core.utils.ExternalAppLauncher
import com.taskttl.data.state.SettingsIntent import com.taskttl.data.state.SettingsIntent
import com.taskttl.data.viewmodel.SettingsViewModel import com.taskttl.data.viewmodel.SettingsViewModel
import com.taskttl.ui.components.AppHeader import com.taskttl.ui.components.AppHeader
@@ -53,29 +51,18 @@ import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.section_data_management import taskttl.composeapp.generated.resources.section_data_management
import taskttl.composeapp.generated.resources.section_general_settings import taskttl.composeapp.generated.resources.section_general_settings
import taskttl.composeapp.generated.resources.section_help_feedback import taskttl.composeapp.generated.resources.section_help_feedback
import taskttl.composeapp.generated.resources.section_social_share
import taskttl.composeapp.generated.resources.setting_about_app import taskttl.composeapp.generated.resources.setting_about_app
import taskttl.composeapp.generated.resources.setting_about_app_desc import taskttl.composeapp.generated.resources.setting_about_app_desc
import taskttl.composeapp.generated.resources.setting_category_management import taskttl.composeapp.generated.resources.setting_category_management
import taskttl.composeapp.generated.resources.setting_category_management_desc import taskttl.composeapp.generated.resources.setting_category_management_desc
import taskttl.composeapp.generated.resources.setting_dark_mode
import taskttl.composeapp.generated.resources.setting_dark_mode_desc
import taskttl.composeapp.generated.resources.setting_data_management
import taskttl.composeapp.generated.resources.setting_data_management_desc
import taskttl.composeapp.generated.resources.setting_feedback import taskttl.composeapp.generated.resources.setting_feedback
import taskttl.composeapp.generated.resources.setting_feedback_desc import taskttl.composeapp.generated.resources.setting_feedback_desc
import taskttl.composeapp.generated.resources.setting_invite_friend
import taskttl.composeapp.generated.resources.setting_invite_friend_desc
import taskttl.composeapp.generated.resources.setting_language
import taskttl.composeapp.generated.resources.setting_language_desc
import taskttl.composeapp.generated.resources.setting_privacy_policy import taskttl.composeapp.generated.resources.setting_privacy_policy
import taskttl.composeapp.generated.resources.setting_privacy_policy_desc import taskttl.composeapp.generated.resources.setting_privacy_policy_desc
import taskttl.composeapp.generated.resources.setting_privacy_rate import taskttl.composeapp.generated.resources.setting_privacy_rate
import taskttl.composeapp.generated.resources.setting_privacy_rate_desc import taskttl.composeapp.generated.resources.setting_privacy_rate_desc
import taskttl.composeapp.generated.resources.setting_push_notification import taskttl.composeapp.generated.resources.setting_push_notification
import taskttl.composeapp.generated.resources.setting_push_notification_desc import taskttl.composeapp.generated.resources.setting_push_notification_desc
import taskttl.composeapp.generated.resources.setting_share_achievement
import taskttl.composeapp.generated.resources.setting_share_achievement_desc
import taskttl.composeapp.generated.resources.title_app_settings import taskttl.composeapp.generated.resources.title_app_settings
/** /**
@@ -86,8 +73,21 @@ import taskttl.composeapp.generated.resources.title_app_settings
@Preview @Preview
fun SettingsScreen( fun SettingsScreen(
navController: NavHostController, navController: NavHostController,
viewModel: SettingsViewModel = koinViewModel() viewModel: SettingsViewModel = koinViewModel(),
) { ) {
val state by viewModel.state.collectAsState()
// 轮询监听权限状态变化
LaunchedEffect(Unit) {
while (true) {
val current = NotificationPermissionManager.verifyPermission()
if (state.isNotification != current) {
viewModel.handleIntent(SettingsIntent.UpdateNotificationStatus(current))
}
kotlinx.coroutines.delay(2000)
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -151,16 +151,15 @@ fun SettingsScreen(
// 通用设置 // 通用设置
// SectionTitle(Icons.Default.Settings, Res.string.section_general_settings) SectionTitle(Icons.Default.Settings, Res.string.section_general_settings)
// var notificationEnabled by remember { mutableStateOf(true) } SettingItem(
// SettingItem( titleRes = Res.string.setting_push_notification,
// titleRes = Res.string.setting_push_notification, descriptionRes = Res.string.setting_push_notification_desc,
// descriptionRes = Res.string.setting_push_notification_desc, showSwitch = true,
// showSwitch = true, switchState = state.isNotification,
// switchState = notificationEnabled, onSwitchChanged = { viewModel.handleIntent(SettingsIntent.RequestPermission) }
// onSwitchChanged = { notificationEnabled = it } )
// )
// var darkMode by remember { mutableStateOf(false) } // var darkMode by remember { mutableStateOf(false) }
// //
@@ -291,7 +290,7 @@ fun SettingItem(
onSwitchChanged: ((Boolean) -> Unit)? = null, onSwitchChanged: ((Boolean) -> Unit)? = null,
showArrow: Boolean = false, showArrow: Boolean = false,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null onClick: (() -> Unit)? = null,
) { ) {
Row( Row(
modifier = modifier modifier = modifier

View File

@@ -55,7 +55,6 @@ import com.taskttl.core.routes.Routes
import com.taskttl.core.ui.ActionButtonListItem import com.taskttl.core.ui.ActionButtonListItem
import com.taskttl.core.ui.ErrorDialog import com.taskttl.core.ui.ErrorDialog
import com.taskttl.core.ui.LoadingOverlay import com.taskttl.core.ui.LoadingOverlay
import com.taskttl.core.utils.ToastUtils import com.taskttl.core.utils.ToastUtils
import com.taskttl.data.local.model.Task import com.taskttl.data.local.model.Task
import com.taskttl.data.state.TaskEffect import com.taskttl.data.state.TaskEffect
@@ -245,7 +244,6 @@ fun TaskScreen(
} }
} }
// 悬浮按钮 // 悬浮按钮
FloatingActionButton( FloatingActionButton(
onClick = { navController.navigate(Routes.Main.Task.AddTask) }, onClick = { navController.navigate(Routes.Main.Task.AddTask) },

View File

@@ -0,0 +1,24 @@
package com.taskttl.core.analytics
import platform.Foundation.NSNumber
import platform.Foundation.NSString
import platform.Foundation.create
import platform.FBSDKCoreKit.*
actual object FacebookEventTracker {
actual fun logEvent(event: FacebookEvent, params: Map<String, Any?>) {
val mutableDict = mutableMapOf<Any?, Any?>()
params.forEach { (key, value) ->
mutableDict[key] = when (value) {
is String -> value as NSString
is Double -> NSNumber(value)
is Int -> NSNumber(value)
is Boolean -> NSNumber(value)
else -> null
}
}
val eventParams = if (mutableDict.isEmpty()) null else mutableDict as Map<Any?, Any?>
AppEvents.shared.logEvent(event.eventName, parameters = eventParams)
}
}

View File

@@ -5,15 +5,15 @@ androidx-appcompat = "1.7.1"
androidx-constraintlayout = "2.2.1" androidx-constraintlayout = "2.2.1"
androidx-core = "1.17.0" androidx-core = "1.17.0"
androidx-espresso = "3.7.0" androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.4" androidx-lifecycle = "2.9.5"
androidx-testExt = "1.3.0" androidx-testExt = "1.3.0"
composeHotReload = "1.0.0-rc02" composeHotReload = "1.0.0-rc02"
composeMultiplatform = "1.9.0" composeMultiplatform = "1.9.1"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.2.20" kotlin = "2.2.20"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
navigationCompose = "2.9.0" navigationCompose = "2.9.1"
koin = "4.1.1" koin = "4.1.1"
ktor = "3.3.1" ktor = "3.3.1"
coil3 = "3.3.0" coil3 = "3.3.0"
@@ -32,16 +32,18 @@ sqlite = "2.6.1"
room = "2.8.2" room = "2.8.2"
ksp = "2.2.20-2.0.2" ksp = "2.2.20-2.0.2"
work = "2.10.5"
# 环境 # 环境
android-compileSdk = "36" android-compileSdk = "36"
android-minSdk = "24" android-minSdk = "24"
android-targetSdk = "36" android-targetSdk = "36"
android-versionCode = "100001" android-versionCode = "100002"
android-versionName = "1.0.1" android-versionName = "1.0.2"
android-facebookAppId = "1203530117944408" android-facebookAppId = "801567145847535"
android-facebookClientToken = "1ee2da9430c1a589e8aa623bfaaaa586" android-facebookClientToken = "15db47cd9d8d35ccaa49f43e30beefaf"
[libraries] [libraries]
@@ -111,6 +113,10 @@ androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", v
# 谷歌Ads # 谷歌Ads
android-play-services-ads-identifier = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" } android-play-services-ads-identifier = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" }
# 安卓任务
androidx-work = {module="androidx.work:work-runtime-ktx", version.ref="work"}
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }