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(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)

View File

@@ -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>

View File

@@ -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()
}

View File

@@ -1,5 +1,6 @@
package com.taskttl
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.os.Bundle
@@ -12,15 +13,14 @@ import org.koin.android.ext.koin.androidLogger
class MainApplication : Application() {
companion object {
lateinit var instance: Application
}
@SuppressLint("StaticFieldLeak")
@Volatile
var currentActivity: Activity? = null
private set
private var _currentActivity: Activity? = null
lateinit var instance: Application
fun currentActivity(): Activity? = _currentActivity
}
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
}
}

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_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>

View File

@@ -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>

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)
}
}
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
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

View File

@@ -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()
}

View File

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

View File

@@ -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

View File

@@ -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) }
}
/**
* 清除错误
*/

View File

@@ -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) }
}
}
}

View File

@@ -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) {

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.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

View File

@@ -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) },

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-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" }