更新
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user