This commit is contained in:
2025-10-08 18:08:15 +08:00
parent dc71bb19a9
commit 989e5be041
109 changed files with 10815 additions and 170 deletions

View File

@@ -3,12 +3,14 @@ package com.taskttl
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
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
App()

View File

@@ -0,0 +1,61 @@
package com.taskttl
import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.google.firebase.FirebaseApp
import com.taskttl.data.di.initKoin
import com.tencent.mmkv.MMKV
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
class MainApplication : Application() {
companion object {
lateinit var instance: Application
}
@Volatile
var currentActivity: Activity? = null
private set
init {
instance = this
}
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {
if (currentActivity == activity) {
currentActivity = null
}
}
// 其他生命周期方法可以留空
override fun onActivityCreated(a: Activity, b: Bundle?) {}
override fun onActivityStarted(a: Activity) {}
override fun onActivityStopped(a: Activity) {}
override fun onActivitySaveInstanceState(a: Activity, b: Bundle) {}
override fun onActivityDestroyed(a: Activity) {}
})
MMKV.initialize(this@MainApplication)
// 初始化 Firebase SDK
FirebaseApp.initializeApp(this@MainApplication)
// 初始化 Koin
initKoin() {
androidLogger()
androidContext(this@MainApplication)
}
}
}

View File

@@ -1,9 +0,0 @@
package com.taskttl
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()

View File

@@ -0,0 +1,176 @@
package com.taskttl.core.ui
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.view.ViewGroup
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import org.jetbrains.compose.resources.stringResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.btn_retry
import taskttl.composeapp.generated.resources.webview_loading_error
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("SetJavaScriptEnabled")
@Composable
actual fun DevTTLWebView(modifier: Modifier, url: String) {
// 状态管理
var isLoading by remember { mutableStateOf(true) }
var hasError by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
var webView by remember { mutableStateOf<WebView?>(null) }
Box(
modifier = Modifier
.fillMaxSize()
) {
val loadingError = stringResource(Res.string.webview_loading_error)
// 使用AndroidView加载WebView
AndroidView(
factory = { ctx ->
WebView(ctx).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// 配置WebView设置
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
// 启用缓存
cacheMode = android.webkit.WebSettings.LOAD_DEFAULT
// 启用混合内容HTTP和HTTPS
mixedContentMode =
android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
}
webViewClient = object : WebViewClient() {
override fun onPageStarted(
view: WebView?,
url: String?,
favicon: Bitmap?
) {
super.onPageStarted(view, url, favicon)
isLoading = true
hasError = false
}
override fun onPageFinished(
view: WebView?,
url: String?
) {
super.onPageFinished(view, url)
isLoading = false
}
// 处理加载错误
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
if (request?.isForMainFrame == true) {
isLoading = false
hasError = true
errorMessage = loadingError
}
}
}
loadUrl(url)
webView = this
}
},
modifier = Modifier.fillMaxSize(),
update = { view ->
// 更新WebView
webView = view
}
)
// 加载指示器
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
// 错误显示
if (hasError) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
Text(
text = errorMessage,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
Button(
onClick = {
hasError = false
webView?.reload()
},
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth(0.5f)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null
)
Text(
text = stringResource(Res.string.btn_retry),
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}
}
// 清理资源
DisposableEffect(Unit) {
onDispose {
webView?.destroy()
}
}
}

View File

@@ -0,0 +1,26 @@
package com.taskttl.core.utils
import android.util.Log as AndroidLog
/**
* 日志
* @author admin
* @date 2025/09/27
*/
actual object LogUtils {
actual fun d(tag: String, message: String) {
AndroidLog.d(tag, message)
}
actual fun i(tag: String, message: String) {
AndroidLog.i(tag, message)
}
actual fun w(tag: String, message: String) {
AndroidLog.w(tag, message)
}
actual fun e(tag: String, message: String, throwable: Throwable?) {
AndroidLog.e(tag, message, throwable)
}
}

View File

@@ -0,0 +1,80 @@
package com.taskttl.core.utils
import com.tencent.mmkv.MMKV
import kotlinx.serialization.json.Json
actual object StorageUtils {
val mmkv: MMKV
get() = MMKV.defaultMMKV()
val json = Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
}
actual fun saveString(key: String, value: String) {
mmkv.encode(key, value)
}
actual fun getString(key: String, defaultValue: String): String {
val value = mmkv.decodeString(key)
if (value.isNullOrEmpty()) {
mmkv.encode(key, defaultValue)
return defaultValue
}
return value
}
actual fun saveInt(key: String, value: Int) {
mmkv.encode(key, value)
}
actual fun getInt(key: String, defaultValue: Int): Int {
return mmkv.decodeInt(key, defaultValue)
}
actual fun saveLong(key: String, value: Long) {
mmkv.encode(key, value)
}
actual fun getLong(key: String, defaultValue: Long): Long {
return mmkv.decodeLong(key, defaultValue)
}
actual fun saveBoolean(key: String, value: Boolean) {
mmkv.encode(key, value)
}
actual fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return mmkv.decodeBool(key, defaultValue)
}
actual inline fun <reified T : Any> saveObject(key: String, value: T) {
val data = json.encodeToString(value)
mmkv.encode(key, data)
}
actual inline fun <reified T : Any> getObject(key: String): T? {
val data = mmkv.decodeString(key) ?: return null
return try {
json.decodeFromString<T>(data)
} catch (e: Exception) {
null
}
}
actual fun contains(key: String): Boolean {
return mmkv.contains(key)
}
actual fun remove(key: String) {
mmkv.removeValueForKey(key)
}
actual fun clear() {
mmkv.clearAll()
}
}

View File

@@ -0,0 +1,10 @@
package com.taskttl.data.di
import com.taskttl.data.local.database.TaskTTLDatabase
import com.taskttl.data.local.database.getDatabaseBuilder
import org.koin.dsl.module
actual fun platformModule() = module {
single<TaskTTLDatabase> { getDatabaseBuilder() }
}

View File

@@ -0,0 +1,17 @@
package com.taskttl.data.local.database
import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.taskttl.MainApplication
import kotlinx.coroutines.Dispatchers
actual fun getDatabaseBuilder(): TaskTTLDatabase {
val context = MainApplication.instance.applicationContext
return Room.databaseBuilder(
context,
TaskTTLDatabase::class.java,
"taskttl_database"
).setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}