更新
This commit is contained in:
@@ -62,7 +62,7 @@ kotlin {
|
||||
implementation(project.dependencies.platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.analytics)
|
||||
// facebook
|
||||
// implementation(libs.android.facebook.android.sdk)
|
||||
implementation(libs.android.facebook.android.sdk)
|
||||
|
||||
// mmkv
|
||||
implementation(libs.android.mmkv)
|
||||
@@ -72,6 +72,9 @@ kotlin {
|
||||
|
||||
// admob
|
||||
implementation(libs.android.play.services.ads.identifier)
|
||||
|
||||
// work
|
||||
implementation(libs.androidx.work)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(compose.runtime)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- 添加网络权限 -->
|
||||
<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" />
|
||||
<!--通知权限-->
|
||||
<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
|
||||
android:name=".MainApplication"
|
||||
@@ -17,9 +23,9 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<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:name=".MainActivity">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -39,6 +45,50 @@
|
||||
</intent-filter>
|
||||
</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>
|
||||
|
||||
<queries>
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
package com.taskttl
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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 {
|
||||
App()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.taskttl
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
@@ -12,16 +13,15 @@ import org.koin.android.ext.koin.androidLogger
|
||||
class MainApplication : Application() {
|
||||
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Volatile
|
||||
private var _currentActivity: Activity? = null
|
||||
|
||||
lateinit var instance: Application
|
||||
|
||||
fun currentActivity(): Activity? = _currentActivity
|
||||
}
|
||||
|
||||
@Volatile
|
||||
var currentActivity: Activity? = null
|
||||
private set
|
||||
|
||||
|
||||
|
||||
|
||||
init {
|
||||
instance = this
|
||||
}
|
||||
@@ -32,12 +32,12 @@ class MainApplication : Application() {
|
||||
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
currentActivity = activity
|
||||
_currentActivity = activity
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (currentActivity == activity) {
|
||||
currentActivity = null
|
||||
if (_currentActivity == activity) {
|
||||
_currentActivity = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -215,9 +215,9 @@
|
||||
<string name="setting_privacy_rate">应用评价</string>
|
||||
<string name="setting_privacy_rate_desc">如果喜欢,欢迎在商店留下五星好评</string>
|
||||
<string name="setting_about_app">关于应用</string>
|
||||
<string name="app_version_code">100001</string>
|
||||
<string name="app_version_name">1.0.1</string>
|
||||
<string name="setting_about_app_desc">版本 1.0.1</string>
|
||||
<string name="app_version_code">100002</string>
|
||||
<string name="app_version_name">1.0.2</string>
|
||||
<string name="setting_about_app_desc">版本 1.0.2</string>
|
||||
|
||||
<!-- 反馈与帮助 -->
|
||||
<string name="title_feedback">意见反馈</string>
|
||||
|
||||
@@ -216,9 +216,9 @@
|
||||
<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_about_app">About the App</string>
|
||||
<string name="app_version_code">100001</string>
|
||||
<string name="app_version_name">1.0.1</string>
|
||||
<string name="setting_about_app_desc">Version 1.0.1</string>
|
||||
<string name="app_version_code">100002</string>
|
||||
<string name="app_version_name">1.0.2</string>
|
||||
<string name="setting_about_app_desc">Version 1.0.2</string>
|
||||
|
||||
<!-- Feedback & Help -->
|
||||
<string name="title_feedback">Feedback</string>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.taskttl.core.analytics
|
||||
|
||||
/**
|
||||
* 统一事件接口
|
||||
*/
|
||||
expect object FacebookEventTracker {
|
||||
fun logEvent(event: FacebookEvent, params: Map<String, Any?> = emptyMap())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,12 @@ object DateUtils {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.taskttl.data.di
|
||||
|
||||
import com.taskttl.core.notification.NotificationManager
|
||||
import com.taskttl.data.repository.OnboardingRepository
|
||||
import com.taskttl.data.repository.SettingsRepository
|
||||
import com.taskttl.data.repository.impl.OnboardingRepositoryImpl
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.taskttl.data.state
|
||||
|
||||
import com.taskttl.core.notification.NotificationPermissionManager
|
||||
import com.taskttl.core.viewmodel.BaseUiState
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ data class SettingsState(
|
||||
override val isLoading: Boolean = false,
|
||||
override val isProcessing: Boolean = false,
|
||||
override val error: String? = null,
|
||||
val isNotification: Boolean = NotificationPermissionManager.verifyPermission(),
|
||||
) : BaseUiState()
|
||||
|
||||
/**
|
||||
@@ -38,6 +40,17 @@ sealed class SettingsIntent {
|
||||
* @param [url] 网址
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ data class TaskState(
|
||||
* @constructor 创建[TaskIntent]
|
||||
*/
|
||||
sealed class TaskIntent {
|
||||
|
||||
/**
|
||||
* 加载任务
|
||||
* @author admin
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.taskttl.data.viewmodel
|
||||
|
||||
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.data.local.model.Category
|
||||
import com.taskttl.data.local.model.CategoryType
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.taskttl.data.viewmodel
|
||||
|
||||
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.LogUtils
|
||||
import com.taskttl.core.viewmodel.BaseViewModel
|
||||
import com.taskttl.data.state.SettingsEffect
|
||||
import com.taskttl.data.state.SettingsIntent
|
||||
@@ -22,6 +25,8 @@ class SettingsViewModel() :
|
||||
when (intent) {
|
||||
is SettingsIntent.OpenAppRating -> openAppRating()
|
||||
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) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.taskttl.data.viewmodel
|
||||
|
||||
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.data.local.model.Category
|
||||
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_update_failed
|
||||
import taskttl.composeapp.generated.resources.task_update_success
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
/**
|
||||
* 任务视图模型
|
||||
@@ -87,10 +92,7 @@ class TaskViewModel(
|
||||
try {
|
||||
launch {
|
||||
categoryRepository.getCategoriesByType(CategoryType.TASK)
|
||||
.collect { categories ->
|
||||
LogUtils.e("DevTTL", categories.toString())
|
||||
updateState { copy(categories = categories) }
|
||||
}
|
||||
.collect { categories -> updateState { copy(categories = categories) } }
|
||||
}
|
||||
launch {
|
||||
taskRepository.getAllTasks().collect { tasks ->
|
||||
@@ -104,7 +106,6 @@ class TaskViewModel(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e("DevTTL", e.message.toString())
|
||||
val errStr = getString(Res.string.task_load_failed)
|
||||
updateState { copy(isLoading = false, error = e.message ?: errStr) }
|
||||
}
|
||||
@@ -131,19 +132,36 @@ class TaskViewModel(
|
||||
* 添加任务
|
||||
* @param [task] 任务
|
||||
*/
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private fun addTask(task: Task) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
if (state.value.isProcessing) return@launch
|
||||
updateState { copy(isLoading = false, isProcessing = true) }
|
||||
// 1️⃣ 插入任务
|
||||
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.NavigateBack)
|
||||
} catch (e: Exception) {
|
||||
val errStr = getString(Res.string.task_add_failed)
|
||||
updateState { copy(error = e.message ?: errStr) }
|
||||
} 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
|
||||
updateState { copy(isLoading = false, isProcessing = true) }
|
||||
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.NavigateBack)
|
||||
} catch (e: Exception) {
|
||||
val errStr = getString(Res.string.task_update_failed)
|
||||
updateState { copy(error = e.message ?: errStr) }
|
||||
} 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
|
||||
updateState { copy(isLoading = false, isProcessing = true) }
|
||||
taskRepository.deleteTask(taskId)
|
||||
NotificationManager.cancelNotification(taskId)
|
||||
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
|
||||
} catch (e: Exception) {
|
||||
val errStr = getString(Res.string.task_delete_failed)
|
||||
updateState { copy(error = e.message ?: errStr) }
|
||||
} finally {
|
||||
updateState { copy(isLoading = false,isProcessing = false) }
|
||||
updateState { copy(isLoading = false, isProcessing = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,10 @@ 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.notification.NotificationPermissionCallback
|
||||
import com.taskttl.core.notification.NotificationPermissionManager
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.utils.LogUtils
|
||||
import com.taskttl.data.local.model.OnboardingPage
|
||||
import com.taskttl.data.state.OnboardingEffect
|
||||
import com.taskttl.data.state.OnboardingIntent
|
||||
@@ -63,6 +66,16 @@ fun OnboardingScreen(
|
||||
val pagerState =
|
||||
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) {
|
||||
viewModel.effects.collectLatest { event ->
|
||||
when (event) {
|
||||
|
||||
@@ -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.Person
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
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
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.notification.NotificationPermissionManager
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.utils.ExternalAppLauncher
|
||||
import com.taskttl.data.state.SettingsIntent
|
||||
import com.taskttl.data.viewmodel.SettingsViewModel
|
||||
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_general_settings
|
||||
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_desc
|
||||
import taskttl.composeapp.generated.resources.setting_category_management
|
||||
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_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_desc
|
||||
import taskttl.composeapp.generated.resources.setting_privacy_rate
|
||||
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_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
|
||||
|
||||
/**
|
||||
@@ -86,8 +73,21 @@ import taskttl.composeapp.generated.resources.title_app_settings
|
||||
@Preview
|
||||
fun SettingsScreen(
|
||||
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(
|
||||
modifier = Modifier
|
||||
.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(
|
||||
// titleRes = Res.string.setting_push_notification,
|
||||
// descriptionRes = Res.string.setting_push_notification_desc,
|
||||
// showSwitch = true,
|
||||
// switchState = notificationEnabled,
|
||||
// onSwitchChanged = { notificationEnabled = it }
|
||||
// )
|
||||
SettingItem(
|
||||
titleRes = Res.string.setting_push_notification,
|
||||
descriptionRes = Res.string.setting_push_notification_desc,
|
||||
showSwitch = true,
|
||||
switchState = state.isNotification,
|
||||
onSwitchChanged = { viewModel.handleIntent(SettingsIntent.RequestPermission) }
|
||||
)
|
||||
|
||||
// var darkMode by remember { mutableStateOf(false) }
|
||||
//
|
||||
@@ -291,7 +290,7 @@ fun SettingItem(
|
||||
onSwitchChanged: ((Boolean) -> Unit)? = null,
|
||||
showArrow: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
||||
@@ -55,7 +55,6 @@ import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.ui.ActionButtonListItem
|
||||
import com.taskttl.core.ui.ErrorDialog
|
||||
import com.taskttl.core.ui.LoadingOverlay
|
||||
|
||||
import com.taskttl.core.utils.ToastUtils
|
||||
import com.taskttl.data.local.model.Task
|
||||
import com.taskttl.data.state.TaskEffect
|
||||
@@ -245,7 +244,6 @@ fun TaskScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 悬浮按钮
|
||||
FloatingActionButton(
|
||||
onClick = { navController.navigate(Routes.Main.Task.AddTask) },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,15 @@ androidx-appcompat = "1.7.1"
|
||||
androidx-constraintlayout = "2.2.1"
|
||||
androidx-core = "1.17.0"
|
||||
androidx-espresso = "3.7.0"
|
||||
androidx-lifecycle = "2.9.4"
|
||||
androidx-lifecycle = "2.9.5"
|
||||
androidx-testExt = "1.3.0"
|
||||
composeHotReload = "1.0.0-rc02"
|
||||
composeMultiplatform = "1.9.0"
|
||||
composeMultiplatform = "1.9.1"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.2.20"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
|
||||
navigationCompose = "2.9.0"
|
||||
navigationCompose = "2.9.1"
|
||||
koin = "4.1.1"
|
||||
ktor = "3.3.1"
|
||||
coil3 = "3.3.0"
|
||||
@@ -32,16 +32,18 @@ sqlite = "2.6.1"
|
||||
room = "2.8.2"
|
||||
ksp = "2.2.20-2.0.2"
|
||||
|
||||
work = "2.10.5"
|
||||
|
||||
# 环境
|
||||
android-compileSdk = "36"
|
||||
android-minSdk = "24"
|
||||
android-targetSdk = "36"
|
||||
|
||||
android-versionCode = "100001"
|
||||
android-versionName = "1.0.1"
|
||||
android-versionCode = "100002"
|
||||
android-versionName = "1.0.2"
|
||||
|
||||
android-facebookAppId = "1203530117944408"
|
||||
android-facebookClientToken = "1ee2da9430c1a589e8aa623bfaaaa586"
|
||||
android-facebookAppId = "801567145847535"
|
||||
android-facebookClientToken = "15db47cd9d8d35ccaa49f43e30beefaf"
|
||||
|
||||
|
||||
[libraries]
|
||||
@@ -111,6 +113,10 @@ androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", v
|
||||
# 谷歌Ads
|
||||
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]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user