diff --git a/README.md b/README.md index bd83943..4232309 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ XuqmGroup-AndroidSDK/ ├── sdk-im/ # IM:WebSocket 实时通信 ├── sdk-push/ # 推送:设备 Token 注册 ├── sdk-update/ # 版本管理:检查更新、下载安装 +├── sdk-webview/ # WebView:嵌入式组件 / 独立页面 └── sample-app/ # 示例 App(Jetpack Compose) ``` @@ -45,6 +46,7 @@ dependencies { implementation("com.xuqm:sdk-im:0.4.0") // 可选 implementation("com.xuqm:sdk-push:0.4.0") // 可选 implementation("com.xuqm:sdk-update:0.4.0") // 可选 + implementation("com.xuqm:sdk-webview:0.4.0") // 可选 } ``` @@ -74,6 +76,34 @@ XuqmSDK.login( // SDK 会自动完成对应模块的登录与初始化 ``` +### 3. WebView 独立模块 + +`sdk-webview` 不需要额外初始化,直接依赖 `sdk-core` 即可使用。 + +```kotlin +import com.xuqm.sdk.webview.XWebViewConfig +import com.xuqm.sdk.webview.XWebViewView +import com.xuqm.sdk.webview.XWebViewScreen +import com.xuqm.sdk.webview.openXWebView + +// 嵌入式组件:直接放到页面中,不包含导航栏 / 状态栏 +XWebViewView( + config = XWebViewConfig( + url = "https://example.com", + title = "嵌入式网页", + ), +) + +// 独立页面:先设置配置,再跳转到页面 +openXWebView( + XWebViewConfig( + url = "https://example.com", + title = "独立页面", + ) +) +// navigate("xwebview") 后使用 XWebViewScreen() +``` + ### 3. 切换联调环境 默认使用外网域名。若要本地联调,可在 `Application.onCreate()` 里切换: @@ -157,6 +187,7 @@ val service = RetrofitFactory.create(MyApiService::class.java) - 支持引用回复、群聊已读人数展示 - 支持 `LOCATION` / `CUSTOM` / `RICH_TEXT` / `FORWARD` / `QUOTE` / `MERGE` / `CALL_AUDIO` / `CALL_VIDEO` 等通用消息类型发送 - 单聊支持已读回执,服务端会把 `READ` 状态推回发送者 +- `sdk-webview` 作为独立模块提供嵌入式组件和独立页面两种形态,可与 IM / Push / Update 任意组合 ### ImClient diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts index ed74206..41b84f2 100644 --- a/sample-app/build.gradle.kts +++ b/sample-app/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":sdk-im")) implementation(project(":sdk-push")) implementation(project(":sdk-update")) + implementation(project(":sdk-webview")) implementation(platform(libs.androidx.compose.bom)) implementation(libs.bundles.compose) diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt index 1b24074..0751218 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt @@ -21,14 +21,14 @@ data class DemoResponse( val message: String? = null, ) -data class LoginRequest(val appId: String, val userId: String, val password: String) +data class LoginRequest(val appKey: String, val userId: String, val password: String) -data class RegisterRequest(val appId: String, val userId: String, val password: String, val nickname: String) +data class RegisterRequest(val appKey: String, val userId: String, val password: String, val nickname: String) -data class ResetPasswordRequest(val appId: String, val userId: String, val newPassword: String) +data class ResetPasswordRequest(val appKey: String, val userId: String, val newPassword: String) data class AuthProfile( - val appId: String, + val appKey: String, val userId: String, val nickname: String, val avatar: String? = null, @@ -68,10 +68,10 @@ interface DemoApi { suspend fun resetPassword(@Body request: ResetPasswordRequest): DemoResponse @GET("api/demo/user/profile") - suspend fun getProfile(@Query("appId") appId: String = DEMO_APP_ID): DemoResponse + suspend fun getProfile(@Query("appKey") appKey: String = DEMO_APP_ID): DemoResponse @GET("api/demo/users/members") - suspend fun listMembers(@Query("appId") appId: String = DEMO_APP_ID): DemoResponse> + suspend fun listMembers(@Query("appKey") appKey: String = DEMO_APP_ID): DemoResponse> @PUT("api/demo/user/profile") suspend fun updateProfile(@Body request: UpdateProfileRequest): DemoResponse @@ -81,7 +81,7 @@ interface DemoApi { @GET("api/demo/users/search") suspend fun searchUsers( - @Query("appId") appId: String = DEMO_APP_ID, + @Query("appKey") appKey: String = DEMO_APP_ID, @Query("keyword") keyword: String, ): DemoResponse> } diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/data/local/LocalImCache.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/data/local/LocalImCache.kt index 63d37d3..c1b15c1 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/data/local/LocalImCache.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/data/local/LocalImCache.kt @@ -136,7 +136,7 @@ class LocalImCache(context: Context) { add( ImMessage( id = obj.optString("id"), - appId = obj.optString("appId"), + appKey = obj.optString("appKey"), fromId = obj.optString("fromId"), toId = obj.optString("toId"), chatType = obj.optString("chatType"), @@ -180,7 +180,7 @@ class LocalImCache(context: Context) { array.put( JSONObject().apply { put("id", message.id) - put("appId", message.appId) + put("appKey", message.appKey) put("fromId", message.fromId) put("toId", message.toId) put("chatType", message.chatType) diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/navigation/AppNavGraph.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/navigation/AppNavGraph.kt index 458265b..1c9e550 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/navigation/AppNavGraph.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/navigation/AppNavGraph.kt @@ -13,6 +13,7 @@ import com.xuqm.sdk.sample.ui.chat.ChatScreen import com.xuqm.sdk.sample.ui.environment.EnvironmentScreen import com.xuqm.sdk.sample.ui.group.GroupSettingsScreen import com.xuqm.sdk.sample.ui.main.MainScreen +import com.xuqm.sdk.webview.XWebViewScreen import java.net.URLDecoder import java.net.URLEncoder @@ -58,6 +59,9 @@ fun AppNavGraph( onGroupSettings = { groupId -> navController.navigate("group_settings/$groupId") }, + onOpenWebView = { + navController.navigate("xwebview") + }, onOpenEnvironment = { navController.navigate("environment") }, onLogout = { navController.navigate("auth") { @@ -67,6 +71,13 @@ fun AppNavGraph( ) } + composable("xwebview") { + XWebViewScreen( + onBack = { navController.popBackStack() }, + onClose = { navController.popBackStack() }, + ) + } + composable("chat/{chatType}/{targetId}/{targetName}") { backStackEntry -> val chatType = backStackEntry.arguments?.getString("chatType") ?: "SINGLE" val targetId = backStackEntry.arguments?.getString("targetId") ?: "" diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt index 92cc841..01a35f3 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt @@ -653,7 +653,7 @@ class ChatViewModel : ViewModel() { private fun mergeMessageRecord(existing: ImMessage, incoming: ImMessage): ImMessage { return existing.copy( - appId = incoming.appId.ifBlank { existing.appId }, + appKey = incoming.appKey.ifBlank { existing.appKey }, fromId = existing.fromId, toId = existing.toId, chatType = existing.chatType, diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/main/MainScreen.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/main/MainScreen.kt index 477e276..58b01eb 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/main/MainScreen.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/main/MainScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Settings @@ -40,6 +41,7 @@ import com.xuqm.sdk.sample.ui.contact.ContactScreen import com.xuqm.sdk.sample.ui.conversation.ConversationScreen import com.xuqm.sdk.sample.ui.group.GroupListScreen import com.xuqm.sdk.sample.ui.profile.ProfileScreen +import com.xuqm.sdk.sample.ui.webview.WebViewEntryScreen import com.xuqm.sdk.sample.ui.update.UpdateScreen import kotlinx.coroutines.launch import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner @@ -51,6 +53,7 @@ private val tabs = listOf( BottomTab("群组", Icons.Default.Group), BottomTab("联系人", Icons.Default.People), BottomTab("更新", Icons.Default.SystemUpdate), + BottomTab("网页", Icons.Default.Language), BottomTab("我", Icons.Default.Person), ) @@ -59,6 +62,7 @@ private val tabs = listOf( fun MainScreen( onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit, onGroupSettings: (groupId: String) -> Unit, + onOpenWebView: () -> Unit, onOpenEnvironment: () -> Unit, onLogout: () -> Unit, ) { @@ -116,7 +120,8 @@ fun MainScreen( ) 2 -> ContactScreen(onOpenChat = { userId -> onOpenChat(userId, "SINGLE", userId) }) 3 -> UpdateScreen() - 4 -> ProfileScreen(onLogout = onLogout) + 4 -> WebViewEntryScreen(onOpenWebView = onOpenWebView) + 5 -> ProfileScreen(onLogout = onLogout) } } } diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/webview/WebViewScreen.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/webview/WebViewScreen.kt new file mode 100644 index 0000000..b12c9b5 --- /dev/null +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/webview/WebViewScreen.kt @@ -0,0 +1,147 @@ +package com.xuqm.sdk.sample.ui.webview + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberSaveable +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.AnnotatedString +import com.xuqm.sdk.webview.XWebViewConfig +import com.xuqm.sdk.webview.XWebViewControl +import com.xuqm.sdk.webview.XWebViewView +import com.xuqm.sdk.webview.openXWebView + +private enum class WebViewMode { Embedded, Page } + +@Composable +fun WebViewEntryScreen( + onOpenWebView: () -> Unit, +) { + var url by rememberSaveable { mutableStateOf("https://example.com") } + var title by rememberSaveable { mutableStateOf("示例网页") } + var mode by rememberSaveable { mutableIntStateOf(WebViewMode.Embedded.ordinal) } + var statusVersion by rememberSaveable { mutableIntStateOf(0) } + val selectedMode = WebViewMode.entries[mode] + val config = XWebViewConfig(url = url.trim(), title = title.trim()) + val clipboard = LocalClipboardManager.current + val currentUrl = remember(statusVersion, config.url) { + XWebViewControl.currentUrl() ?: config.url + } + + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text("XWebView", style = MaterialTheme.typography.headlineMedium) + Text( + "可在下面输入 URL,切换嵌入式组件或独立页面模式。", + style = MaterialTheme.typography.bodyMedium, + ) + + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text("URL") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + OutlinedTextField( + value = title, + onValueChange = { title = it }, + label = { Text("标题") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button(onClick = { mode = WebViewMode.Embedded.ordinal }) { + Text(if (selectedMode == WebViewMode.Embedded) "嵌入式" else "切换到嵌入式") + } + Button(onClick = { mode = WebViewMode.Page.ordinal }) { + Text(if (selectedMode == WebViewMode.Page) "页面模式" else "切换到页面") + } + } + + if (selectedMode == WebViewMode.Embedded) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + Text("嵌入式组件", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(12.dp)) + XWebViewView( + config = config, + modifier = Modifier + .fillMaxWidth() + .height(320.dp), + ) + } + } + } + + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("页面控制", style = MaterialTheme.typography.titleMedium) + Text("当前 URL: ${if (currentUrl.isBlank()) "未加载" else currentUrl}") + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button( + onClick = { + XWebViewControl.goBack() + statusVersion++ + }, + enabled = XWebViewControl.canGoBack(), + ) { Text("后退") } + + Button( + onClick = { + XWebViewControl.goForward() + statusVersion++ + }, + enabled = XWebViewControl.canGoForward(), + ) { Text("前进") } + + Button(onClick = { + XWebViewControl.reload() + statusVersion++ + }) { Text("刷新") } + } + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button(onClick = { + clipboard.setText(AnnotatedString(currentUrl.ifBlank { config.url })) + statusVersion++ + }) { Text("复制 URL") } + + Button(onClick = { + XWebViewControl.loadUrl(url.trim()) + statusVersion++ + }) { Text("重新加载输入 URL") } + } + } + } + + Button(onClick = { + openXWebView(config) + onOpenWebView() + }) { + Text("打开页面模式") + } + } +} diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index 6b72587..655e94e 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -23,6 +23,8 @@ object XuqmSDK { private var initialized = false @Volatile + private var initializedAppKey: String? = null + @Volatile private var loginSession: XuqmLoginSession? = null fun initialize( @@ -30,11 +32,22 @@ object XuqmSDK { appKey: String, logLevel: LogLevel = LogLevel.WARN, ) { - config = SDKConfig(appKey, logLevel) - appContext = context.applicationContext - tokenStore = TokenStore(context.applicationContext) - ApiClient.init(config, tokenStore) - initialized = true + val applicationContext = context.applicationContext + synchronized(this) { + if (initialized) { + check(initializedAppKey == appKey) { + "XuqmSDK already initialized with appKey=$initializedAppKey" + } + appContext = applicationContext + return + } + config = SDKConfig(appKey, logLevel) + appContext = applicationContext + tokenStore = TokenStore(applicationContext) + ApiClient.init(config, tokenStore) + initializedAppKey = appKey + initialized = true + } } fun configureServiceEndpoints(endpoints: ServiceEndpoints) { @@ -65,6 +78,9 @@ object XuqmSDK { userSig: String, ): XuqmLoginSession = withContext(Dispatchers.IO) { requireInit() + loginSession?.takeIf { + it.appKey == appKey && it.userId == userId && it.userSig == userSig + }?.let { return@withContext it } val session = XuqmLoginSession( appKey = appKey, userId = userId, diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt index 25d32bc..1b1a848 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt @@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit class ImClient( private val wsUrl: String, private val token: String, - private val appId: String, + private val appKey: String, ) { companion object { private const val TAG = "XuqmImClient" @@ -36,7 +36,7 @@ class ImClient( .build() fun connect() { - Log.d(TAG, "connect() wsUrl=$wsUrl appId=$appId") + Log.d(TAG, "connect() wsUrl=$wsUrl appKey=$appKey") disconnect(closeSocket = false) val request = Request.Builder() .url(wsUrl) @@ -95,7 +95,7 @@ class ImClient( ): Boolean { Log.d(TAG, "sendMessage messageId=$messageId toId=$toId chatType=$chatType msgType=$msgType contentLength=${content.length} mentioned=${mentionedUserIds.orEmpty()}") val payload = linkedMapOf( - "appId" to appId, + "appKey" to appKey, "messageId" to messageId, "toId" to toId, "chatType" to chatType, @@ -124,7 +124,7 @@ class ImClient( ), gson.toJson( mapOf( - "appId" to appId, + "appKey" to appKey, "messageId" to messageId, ) ), @@ -138,7 +138,7 @@ class ImClient( "destination" to "/app/chat.sync", "content-type" to "application/json", ), - gson.toJson(mapOf("appId" to appId)), + gson.toJson(mapOf("appKey" to appKey)), ) } diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt index 73506fe..38bb440 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt @@ -93,6 +93,8 @@ object ImSDK { var currentUserId: String = "" private set + @Volatile + private var currentUserSig: String? = null interface ConversationListener { fun onConversationsChanged(conversations: List) @@ -111,7 +113,11 @@ object ImSDK { suspend fun login(userId: String, userSig: String) = withContext(Dispatchers.IO) { XuqmSDK.requireInit() + if (currentUserId == userId && currentUserSig == userSig) { + return@withContext + } currentUserId = userId + currentUserSig = userSig connectWithToken(userSig) } @@ -801,7 +807,9 @@ object ImSDK { fun onSdkLogin(session: XuqmLoginSession) { XuqmSDK.requireInit() + if (currentUserId == session.userId && currentUserSig == session.userSig) return currentUserId = session.userId + currentUserSig = session.userSig connectWithToken(session.userSig) } @@ -838,6 +846,7 @@ object ImSDK { client?.disconnect() client = null currentUserId = "" + currentUserSig = null currentToken = "" synchronized(activeGroupSubscriptions) { activeGroupSubscriptions.clear() @@ -896,7 +905,7 @@ object ImSDK { ): ImMessage { return ImMessage( id = messageId, - appId = XuqmSDK.appKey, + appKey = XuqmSDK.appKey, fromId = currentUserId, toId = toId, chatType = chatType, diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt index 492fb38..303e2fd 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt @@ -32,7 +32,7 @@ data class ApiResponse( ) data class LoginRequest( - val appId: String, + val appKey: String, val userId: String, val nickname: String? = null, val avatar: String? = null, @@ -64,7 +64,7 @@ interface ImApi { @GET("api/im/messages/history/{toId}") suspend fun fetchHistory( @Path("toId") toId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("msgType") msgType: String? = null, @Query("keyword") keyword: String? = null, @Query("startTime") startTime: String? = null, @@ -76,7 +76,7 @@ interface ImApi { @GET("api/im/messages/group-history/{groupId}") suspend fun fetchGroupHistory( @Path("groupId") groupId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("msgType") msgType: String? = null, @Query("keyword") keyword: String? = null, @Query("startTime") startTime: String? = null, @@ -86,31 +86,31 @@ interface ImApi { ): ApiResponse> @GET("api/im/groups") - suspend fun listGroups(@Query("appId") appId: String): ApiResponse> + suspend fun listGroups(("appKey") appKey: String): ApiResponse> @GET("api/im/groups/public") suspend fun listPublicGroups( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("keyword") keyword: String? = null, ): ApiResponse> @GET("api/im/admin/users/search") suspend fun searchUsers( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("keyword") keyword: String, @Query("size") size: Int = 20, ): ApiResponse> @GET("api/im/admin/groups/search") suspend fun searchGroups( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("keyword") keyword: String, @Query("size") size: Int = 20, ): ApiResponse> @GET("api/im/admin/messages/search") suspend fun searchMessages( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("keyword") keyword: String? = null, @Query("chatType") chatType: String? = null, @Query("msgType") msgType: String? = null, @@ -122,7 +122,7 @@ interface ImApi { @POST("api/im/groups") suspend fun createGroup( - @Query("appId") appId: String, + ("appKey") appKey: String, @Body request: CreateGroupRequest, ): ApiResponse @@ -132,13 +132,13 @@ interface ImApi { @GET("api/im/groups/{groupId}/members") suspend fun listGroupMembers( @Path("groupId") groupId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse> @GET("api/im/groups/{groupId}/members/search") suspend fun searchGroupMembers( @Path("groupId") groupId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("keyword") keyword: String, @Query("size") size: Int = 20, ): ApiResponse> @@ -200,73 +200,73 @@ interface ImApi { @POST("api/im/groups/{groupId}/join-requests") suspend fun sendGroupJoinRequest( @Path("groupId") groupId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("remark") remark: String? = null, ): ApiResponse @GET("api/im/groups/{groupId}/join-requests") suspend fun listGroupJoinRequests( @Path("groupId") groupId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse> @POST("api/im/groups/{groupId}/join-requests/{requestId}/accept") suspend fun acceptGroupJoinRequest( @Path("groupId") groupId: String, @Path("requestId") requestId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @POST("api/im/groups/{groupId}/join-requests/{requestId}/reject") suspend fun rejectGroupJoinRequest( @Path("groupId") groupId: String, @Path("requestId") requestId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @GET("api/im/friends") - suspend fun listFriends(@Query("appId") appId: String): ApiResponse> + suspend fun listFriends(("appKey") appKey: String): ApiResponse> @POST("api/im/friends") suspend fun addFriend( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("friendId") friendId: String, ): ApiResponse @DELETE("api/im/friends") - suspend fun removeAllFriends(@Query("appId") appId: String): ApiResponse + suspend fun removeAllFriends(("appKey") appKey: String): ApiResponse @DELETE("api/im/friends/{friendId}") suspend fun removeFriend( @Path("friendId") friendId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @PUT("api/im/friends/{friendId}/group") suspend fun setFriendGroup( @Path("friendId") friendId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("groupName") groupName: String? = null, ): ApiResponse @GET("api/im/friends/groups") - suspend fun listFriendGroups(@Query("appId") appId: String): ApiResponse> + suspend fun listFriendGroups(("appKey") appKey: String): ApiResponse> @GET("api/im/friends/groups/{groupName}") suspend fun listFriendsByGroup( @Path("groupName") groupName: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse> @GET("api/im/friend-requests") suspend fun listFriendRequests( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("direction") direction: String = "incoming", ): ApiResponse> @POST("api/im/friend-requests") suspend fun sendFriendRequest( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("toUserId") toUserId: String, @Query("remark") remark: String? = null, ): ApiResponse @@ -274,58 +274,58 @@ interface ImApi { @POST("api/im/friend-requests/{requestId}/accept") suspend fun acceptFriendRequest( @Path("requestId") requestId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @POST("api/im/friend-requests/{requestId}/reject") suspend fun rejectFriendRequest( @Path("requestId") requestId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @GET("api/im/blacklist") - suspend fun listBlacklist(@Query("appId") appId: String): ApiResponse> + suspend fun listBlacklist(("appKey") appKey: String): ApiResponse> @POST("api/im/blacklist") suspend fun addToBlacklist( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("blockedUserId") blockedUserId: String, ): ApiResponse @DELETE("api/im/blacklist") suspend fun removeFromBlacklist( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("blockedUserId") blockedUserId: String, ): ApiResponse @GET("api/im/blacklist/check") suspend fun checkBlacklist( - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("targetUserId") targetUserId: String, ): ApiResponse @GET("api/im/accounts/{userId}") suspend fun getProfile( @Path("userId") userId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @PUT("api/im/accounts/{userId}") suspend fun updateProfile( @Path("userId") userId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("nickname") nickname: String? = null, @Query("avatar") avatar: String? = null, @Query("gender") gender: String? = null, ): ApiResponse @GET("api/im/conversations") - suspend fun listConversations(@Query("appId") appId: String): ApiResponse> + suspend fun listConversations(("appKey") appKey: String): ApiResponse> @PUT("api/im/conversations/{targetId}/pinned") suspend fun setConversationPinned( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, @Query("pinned") pinned: Boolean, ): ApiResponse @@ -333,7 +333,7 @@ interface ImApi { @PUT("api/im/conversations/{targetId}/muted") suspend fun setConversationMuted( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, @Query("muted") muted: Boolean, ): ApiResponse @@ -341,7 +341,7 @@ interface ImApi { @PUT("api/im/conversations/{targetId}/hidden") suspend fun setConversationHidden( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, @Query("hidden") hidden: Boolean, ): ApiResponse @@ -349,44 +349,44 @@ interface ImApi { @PUT("api/im/conversations/{targetId}/group") suspend fun setConversationGroup( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, @Query("groupName") groupName: String? = null, ): ApiResponse @GET("api/im/conversation-groups") - suspend fun listConversationGroups(@Query("appId") appId: String): ApiResponse> + suspend fun listConversationGroups(("appKey") appKey: String): ApiResponse> @GET("api/im/conversation-groups/{groupName}") suspend fun listConversationGroupItems( @Path("groupName") groupName: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse> @PUT("api/im/conversations/{targetId}/read") suspend fun markRead( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, ): ApiResponse @PUT("api/im/messages/{messageId}") suspend fun editMessage( @Path("messageId") messageId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Body request: EditMessageRequest, ): ApiResponse @POST("api/im/messages/{messageId}/revoke") suspend fun revokeMessage( @Path("messageId") messageId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, ): ApiResponse @PUT("api/im/conversations/{targetId}/draft") suspend fun setDraft( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, @Query("draft") draft: String, ): ApiResponse @@ -394,47 +394,47 @@ interface ImApi { @DELETE("api/im/conversations/{targetId}") suspend fun deleteConversation( @Path("targetId") targetId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Query("chatType") chatType: String, ): ApiResponse @POST("api/im/admin/groups/{groupId}/read-receipts") suspend fun adminGroupReadReceipts( @Path("groupId") groupId: String, - @Query("appId") appId: String, + ("appKey") appKey: String, @Body request: GroupReadReceiptRequest, ): ApiResponse> @POST("api/im/friends/batch") - suspend fun batchAddFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse + suspend fun batchAddFriends(("appKey") appKey: String, @Body request: BatchFriendRequest): ApiResponse @POST("api/im/friends/batch/remove") - suspend fun batchRemoveFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse + suspend fun batchRemoveFriends(("appKey") appKey: String, @Body request: BatchFriendRequest): ApiResponse @POST("api/im/friend-requests/batch/accept") - suspend fun batchAcceptFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + suspend fun batchAcceptFriendRequests(("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse @POST("api/im/friend-requests/batch/reject") - suspend fun batchRejectFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + suspend fun batchRejectFriendRequests(("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse @POST("api/im/groups/{groupId}/members/batch") - suspend fun batchAddGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse + suspend fun batchAddGroupMembers(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchUserIds): ApiResponse @POST("api/im/groups/{groupId}/members/batch/remove") - suspend fun batchRemoveGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse + suspend fun batchRemoveGroupMembers(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchUserIds): ApiResponse @POST("api/im/groups/{groupId}/join-requests/batch/accept") - suspend fun batchAcceptGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + suspend fun batchAcceptGroupJoinRequests(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse @POST("api/im/groups/{groupId}/join-requests/batch/reject") - suspend fun batchRejectGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + suspend fun batchRejectGroupJoinRequests(@Path("groupId") groupId: String, ("appKey") appKey: String, @Body request: BatchRequestIds): ApiResponse @PUT("api/im/groups/{groupId}/members/{userId}/info") - suspend fun modifyGroupMemberInfo(@Path("groupId") groupId: String, @Path("userId") userId: String, @Query("appId") appId: String, @Body request: ModifyMemberInfoRequest): ApiResponse + suspend fun modifyGroupMemberInfo(@Path("groupId") groupId: String, @Path("userId") userId: String, ("appKey") appKey: String, @Body request: ModifyMemberInfoRequest): ApiResponse @GET("api/im/messages/offline/count") - suspend fun offlineMessageCount(@Query("appId") appId: String): ApiResponse> + suspend fun offlineMessageCount(("appKey") appKey: String): ApiResponse> @POST("api/im/messages/offline") - suspend fun syncOfflineMessages(@Query("appId") appId: String): ApiResponse> + suspend fun syncOfflineMessages(("appKey") appKey: String): ApiResponse> } diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt index 161a0f8..b00f15f 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt @@ -20,7 +20,7 @@ data class EditMessageRequest( data class ImMessage( val id: String, - val appId: String, + val appKey: String, @SerializedName(value = "fromId", alternate = ["fromUserId"]) val fromId: String, val toId: String, @@ -74,7 +74,7 @@ data class UserProfile( data class FriendRequest( val id: String, - val appId: String, + val appKey: String, val fromUserId: String, val toUserId: String, val remark: String? = null, @@ -85,7 +85,7 @@ data class FriendRequest( data class GroupJoinRequest( val id: String, - val appId: String, + val appKey: String, val groupId: String, val requesterId: String, val remark: String? = null, @@ -96,7 +96,7 @@ data class GroupJoinRequest( data class BlacklistEntry( val id: String, - val appId: String, + val appKey: String, val userId: String, val blockedUserId: String, val createdAt: Long, diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt index 8a68fb5..e017254 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt @@ -113,7 +113,7 @@ object PushSDK { scope.launch { runCatching { api.setReceivePush( - appId = XuqmSDK.appKey, + appKey = XuqmSDK.appKey, userId = resolvedUserId, deviceId = DeviceUtils.getDeviceId(context), enabled = enabled, @@ -155,7 +155,7 @@ object PushSDK { scope.launch { runCatching { api.registerDevice( - appId = XuqmSDK.appKey, + appKey = XuqmSDK.appKey, userId = userId, vendor = vendor.name, token = pushToken, diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt index 08a65c2..623fe75 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt @@ -8,7 +8,7 @@ interface PushApi { @POST("api/push/register") suspend fun registerDevice( - @Query("appId") appId: String, + @Query("appKey") appKey: String, @Query("userId") userId: String, @Query("vendor") vendor: String, @Query("token") token: String, @@ -22,7 +22,7 @@ interface PushApi { @DELETE("api/push/unregister") suspend fun unregisterDevice( - @Query("appId") appId: String, + @Query("appKey") appKey: String, @Query("userId") userId: String, @Query("vendor") vendor: String, @Query("deviceId") deviceId: String? = null, @@ -30,7 +30,7 @@ interface PushApi { @POST("api/push/receive-push") suspend fun setReceivePush( - @Query("appId") appId: String, + @Query("appKey") appKey: String, @Query("userId") userId: String, @Query("deviceId") deviceId: String? = null, @Query("enabled") enabled: Boolean, diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt index 66e27b5..a1a6440 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt @@ -7,7 +7,7 @@ import retrofit2.http.Query interface PushConfigApi { @GET("api/sdk/config") suspend fun sdkConfig( - @Query("appId") appId: String, + @Query("appKey") appKey: String, @Query("platform") platform: String = "ANDROID", ): SdkConfigResponse } diff --git a/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt b/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt index 23df221..818f31d 100644 --- a/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt +++ b/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt @@ -9,7 +9,7 @@ data class ApiResponse(val code: Int, val data: T?, val message: String) interface UpdateApi { @GET("api/v1/updates/app/check") suspend fun checkUpdate( - @Query("appId") appId: String, + @Query("appKey") appKey: String, @Query("platform") platform: String, @Query("currentVersionCode") currentVersionCode: Int, ): ApiResponse diff --git a/sdk-webview/build.gradle.kts b/sdk-webview/build.gradle.kts new file mode 100644 index 0000000..1b63937 --- /dev/null +++ b/sdk-webview/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.compose) +} + +apply(from = rootProject.file("gradle/publish.gradle")) + +android { + namespace = "com.xuqm.sdk.webview" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + buildFeatures { + compose = true + } + + publishing { + singleVariant("release") + } +} + +dependencies { + api(project(":sdk-core")) + api(platform(libs.androidx.compose.bom)) + api(libs.androidx.ui) + api(libs.androidx.ui.graphics) + api(libs.androidx.ui.tooling.preview) + api(libs.androidx.material3) + api(libs.androidx.material.icons.extended) + api(libs.androidx.webkit) + implementation(libs.androidx.activity.compose) +} diff --git a/sdk-webview/consumer-rules.pro b/sdk-webview/consumer-rules.pro new file mode 100644 index 0000000..81393d5 --- /dev/null +++ b/sdk-webview/consumer-rules.pro @@ -0,0 +1 @@ +# Keep module rules intentionally empty for now. diff --git a/sdk-webview/src/main/AndroidManifest.xml b/sdk-webview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..94cbbcf --- /dev/null +++ b/sdk-webview/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt new file mode 100644 index 0000000..538d177 --- /dev/null +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt @@ -0,0 +1,39 @@ +package com.xuqm.sdk.webview + +import androidx.compose.runtime.mutableStateOf + +private val currentConfig = mutableStateOf(XWebViewConfig()) +private var currentController: XWebViewController? = null + +fun openXWebView(config: XWebViewConfig) { + currentConfig.value = config +} + +fun getXWebViewConfig(): XWebViewConfig = currentConfig.value + +fun setXWebViewController(controller: XWebViewController?) { + currentController = controller +} + +fun getXWebViewController(): XWebViewController? = currentController + +object XWebViewControl : XWebViewController { + override fun canGoBack(): Boolean = currentController?.canGoBack() ?: false + override fun canGoForward(): Boolean = currentController?.canGoForward() ?: false + override fun currentUrl(): String? = currentController?.currentUrl() + override fun goBack() { + currentController?.goBack() + } + + override fun goForward() { + currentController?.goForward() + } + + override fun reload() { + currentController?.reload() + } + + override fun loadUrl(url: String) { + currentController?.loadUrl(url) + } +} diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewScreen.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewScreen.kt new file mode 100644 index 0000000..29f3563 --- /dev/null +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewScreen.kt @@ -0,0 +1,69 @@ +package com.xuqm.sdk.webview + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun XWebViewScreen( + modifier: Modifier = Modifier, + config: XWebViewConfig = getXWebViewConfig(), + onBack: (() -> Unit)? = null, + onClose: (() -> Unit)? = null, +) { + val controller = XWebViewControl + val canGoBack = controller.canGoBack() + + BackHandler(enabled = canGoBack) { + controller.goBack() + } + + Scaffold( + modifier = modifier, + topBar = { + if (!config.hideToolbar) { + TopAppBar( + title = { Text(text = config.title.ifBlank { "WebView" }) }, + navigationIcon = { + IconButton(onClick = { + if (canGoBack) { + controller.goBack() + } else { + onBack?.invoke() ?: onClose?.invoke() + } + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }, + actions = { + if (onClose != null) { + IconButton(onClick = onClose) { + Icon(Icons.Default.Close, contentDescription = "Close") + } + } + }, + ) + } + }, + ) { paddingValues -> + Column(modifier = Modifier.fillMaxSize()) { + XWebViewView( + modifier = Modifier.fillMaxSize(), + config = config, + ) + } + } +} diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewTypes.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewTypes.kt new file mode 100644 index 0000000..c559149 --- /dev/null +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewTypes.kt @@ -0,0 +1,19 @@ +package com.xuqm.sdk.webview + +data class XWebViewConfig( + val url: String = "", + val title: String = "", + val hideToolbar: Boolean = false, + val hideStatusBar: Boolean = false, + val userAgent: String? = null, +) + +interface XWebViewController { + fun canGoBack(): Boolean + fun canGoForward(): Boolean + fun currentUrl(): String? + fun goBack() + fun goForward() + fun reload() + fun loadUrl(url: String) +} diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt new file mode 100644 index 0000000..e67cdb9 --- /dev/null +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt @@ -0,0 +1,86 @@ +package com.xuqm.sdk.webview + +import android.annotation.SuppressLint +import android.webkit.WebResourceRequest +import android.webkit.WebViewClient +import android.webkit.WebView +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun XWebViewView( + modifier: Modifier = Modifier, + config: XWebViewConfig = getXWebViewConfig(), +) { + var webView by remember { mutableStateOf(null) } + var currentUrl by remember { mutableStateOf(config.url.ifBlank { null }) } + DisposableEffect(Unit) { + onDispose { + if (getXWebViewController() != null) { + setXWebViewController(null) + } + webView?.destroy() + webView = null + } + } + + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + config.userAgent?.let { settings.userAgentString = it } + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + currentUrl = url ?: view?.url?.toString() + super.onPageFinished(view, url) + } + + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + return false + } + } + if (config.url.isNotBlank()) { + loadUrl(config.url) + } + webView = this + } + }, + update = { view -> + if (view.url.isNullOrBlank() && config.url.isNotBlank()) { + view.loadUrl(config.url) + } + }, + ) + + SideEffect { + val view = webView ?: return@SideEffect + setXWebViewController(object : XWebViewController { + override fun canGoBack(): Boolean = view.canGoBack() + override fun canGoForward(): Boolean = view.canGoForward() + override fun currentUrl(): String? = currentUrl ?: view.url + override fun goBack() { + if (view.canGoBack()) view.goBack() + } + override fun goForward() { + if (view.canGoForward()) view.goForward() + } + override fun reload() { + view.reload() + } + override fun loadUrl(url: String) { + view.loadUrl(url) + } + }) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c469d64..ebd9a21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,4 +24,5 @@ include(":sdk-core") include(":sdk-im") include(":sdk-push") include(":sdk-update") +include(":sdk-webview") include(":sample-app")