diff --git a/app/src/main/assets/template/tool-view.html b/app/src/main/assets/template/tool-view.html
index 1a12873..7ff13b2 100644
--- a/app/src/main/assets/template/tool-view.html
+++ b/app/src/main/assets/template/tool-view.html
@@ -1,18 +1,18 @@
-
-
-
- Preview
-
-
-
-
-
-
+
+
+
+ Preview
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt
index 5636677..5a21f81 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt
@@ -10,22 +10,21 @@ import android.util.Log
import android.webkit.ConsoleMessage
import android.webkit.ValueCallback
import android.webkit.WebView
+import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -59,12 +58,14 @@ internal fun ToolViewRoute(
viewModel: ToolViewScreenViewModel = hiltViewModel(),
onBackClick: () -> Unit
) {
- val isPreview by viewModel.isPreview.collectAsStateWithLifecycle()
val toolViewUiState by viewModel.toolViewUiState.collectAsStateWithLifecycle()
+ val webViewInstanceState by viewModel.webviewInstance.collectAsStateWithLifecycle()
+ val isPreview by viewModel.isPreview.collectAsStateWithLifecycle()
ToolViewScreen(
modifier = modifier,
toolViewUiState = toolViewUiState,
+ webViewInstanceState = webViewInstanceState,
isPreview = isPreview,
onBackClick = onBackClick
)
@@ -76,68 +77,79 @@ internal fun ToolViewRoute(
internal fun ToolViewScreen(
modifier: Modifier = Modifier,
toolViewUiState: ToolViewUiState,
+ webViewInstanceState: WebViewInstanceState,
isPreview: Boolean,
onBackClick: () -> Unit
) {
+
+ Column(
+ modifier
+ .fillMaxWidth()
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(
+ WindowInsetsSides.Horizontal
+ )
+ )
+ ) {
+ TopBar(
+ toolViewUiState = toolViewUiState,
+ isPreview = isPreview,
+ onBackClick = onBackClick
+ )
+ Content(
+ toolViewUiState = toolViewUiState,
+ webViewInstanceState = webViewInstanceState
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopBar(
+ toolViewUiState: ToolViewUiState,
+ isPreview: Boolean,
+ onBackClick: () -> Unit
+) = OxygenTopAppBar(
+ title = {
+ Text(
+ text = when (toolViewUiState) {
+ ToolViewUiState.Loading -> stringResource(R.string.core_loading)
+ ToolViewUiState.Error -> stringResource(R.string.feature_tools_can_not_open)
+ is ToolViewUiState.Success -> if (isPreview) stringResource(
+ R.string.feature_tool_view_preview_suffix,
+ toolViewUiState.toolName
+ ) else toolViewUiState.toolName
+ }
+ )
+ },
+ navigationIcon = OxygenIcons.Back,
+ navigationIconContentDescription = stringResource(R.string.core_back),
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ containerColor = Color.Transparent,
+ scrolledContainerColor = Color.Transparent
+ ),
+ onNavigationClick = onBackClick
+)
+
+@Composable
+private fun Content(
+ toolViewUiState: ToolViewUiState,
+ webViewInstanceState: WebViewInstanceState
+) {
val context = LocalContext.current
var fileChooserCallback by remember { mutableStateOf>?>(null) }
- val fileChooserLauncher =
- rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == Activity.RESULT_OK) {
- it.data?.data?.let { uri ->
- fileChooserCallback?.onReceiveValue(arrayOf(uri))
- } ?: {
- fileChooserCallback?.onReceiveValue(emptyArray())
- }
- } else {
- fileChooserCallback?.onReceiveValue(emptyArray())
- }
+ val fileChooserLauncher = rememberFileChooserLauncher(fileChooserCallback)
+
+ val permissionLauncher = rememberPermissionLauncher()
+
+ when (webViewInstanceState) {
+ WebViewInstanceState.Loading -> {
+ Indicator()
}
- val permissionLauncher =
- rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
- Permissions.continuation?.resume(it)
- }
-
- Scaffold(
- modifier = Modifier,
- containerColor = Color.Transparent,
- contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0)
- ) { padding ->
- Column(
- modifier
- .fillMaxWidth()
- .padding(padding)
- .consumeWindowInsets(padding)
- .windowInsetsPadding(
- WindowInsets.safeDrawing.only(
- WindowInsetsSides.Horizontal
- )
- )
- ) {
- OxygenTopAppBar(
- title = {
- Text(
- text = when (toolViewUiState) {
- ToolViewUiState.Loading -> stringResource(R.string.core_loading)
- ToolViewUiState.Error -> stringResource(R.string.feature_tools_can_not_open)
- is ToolViewUiState.Success -> if (isPreview) stringResource(
- R.string.feature_tool_view_preview_suffix,
- toolViewUiState.toolName
- ) else toolViewUiState.toolName
- }
- )
- },
- navigationIcon = OxygenIcons.Back,
- navigationIconContentDescription = stringResource(R.string.core_back),
- colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
- containerColor = Color.Transparent,
- scrolledContainerColor = Color.Transparent
- ),
- onNavigationClick = onBackClick
- )
+ is WebViewInstanceState.Success -> {
when (toolViewUiState) {
ToolViewUiState.Loading -> {
Indicator()
@@ -162,79 +174,18 @@ internal fun ToolViewScreen(
.fillMaxSize()
.imePadding(),
state = webViewState,
- chromeClient = remember {
- object : AccompanistWebChromeClient() {
- override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
- consoleMessage?.let {
- Timber.tag("WebView").log(
- priority = when (it.messageLevel()) {
- ConsoleMessage.MessageLevel.TIP -> Log.VERBOSE
- ConsoleMessage.MessageLevel.LOG -> Log.INFO
- ConsoleMessage.MessageLevel.WARNING -> Log.WARN
- ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
- else -> Log.DEBUG
- },
- message = "${it.message()} (${it.lineNumber()})"
- )
- return true
- }
- return false
- }
-
- override fun onShowFileChooser(
- webView: WebView?,
- filePathCallback: ValueCallback>?,
- fileChooserParams: FileChooserParams?
- ): Boolean {
- fileChooserCallback = filePathCallback
- val intent = fileChooserParams?.createIntent()
- ?: Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "*/*"
- }
- fileChooserLauncher.launch(
- Intent.createChooser(
- intent,
- fileChooserParams?.title ?: ResourcesUtils.getString(
- context = context,
- resId = R.string.core_file_select_one_text
- )
- )
- )
-
- return true
- }
- }
+ chromeClient = rememberChromeClient(
+ context = context,
+ fileChooserLauncher = fileChooserLauncher
+ ) {
+ fileChooserCallback = it
},
- onCreated = {
- it.settings.javaScriptEnabled = true
- it.settings.domStorageEnabled = true
- it.addJavascriptInterface(
- NativeWebApi(context = context, webView = it, permissionLauncher),
- "NativeApi"
- )
- it.setDownloadListener { url, userAgent, _, mimetype, _ ->
- if (!listOf("http://", "https://").any(url::startsWith)) {
- it.evaluateJavascript(
- "alert('${
- ResourcesUtils.getString(
- context = context,
- resId = R.string.core_can_only_download_http_https,
- url
- )
- }')"
- ) {}
- return@setDownloadListener
- }
- val request = DownloadManager.Request(Uri.parse(url)).apply {
- addRequestHeader("User-Agent", userAgent)
- setMimeType(mimetype)
- setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
- }
- val downloadManager: DownloadManager =
- context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
- downloadManager.enqueue(request)
- }
+ onCreated = initWebView(
+ context = context,
+ permissionLauncher = permissionLauncher
+ ),
+ factory = {
+ webViewInstanceState.webView
}
)
}
@@ -242,3 +193,108 @@ internal fun ToolViewScreen(
}
}
}
+
+@Composable
+private fun rememberFileChooserLauncher(fileChooserCallback: ValueCallback>?) =
+ rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ it.data?.data?.let { uri ->
+ fileChooserCallback?.onReceiveValue(arrayOf(uri))
+ } ?: {
+ fileChooserCallback?.onReceiveValue(emptyArray())
+ }
+ } else {
+ fileChooserCallback?.onReceiveValue(emptyArray())
+ }
+ }
+
+@Composable
+private fun rememberPermissionLauncher() =
+ rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
+ Permissions.continuation?.resume(it)
+ }
+
+@SuppressLint("SetJavaScriptEnabled")
+private fun initWebView(
+ context: Context,
+ permissionLauncher: ManagedActivityResultLauncher
+) = { webview: WebView ->
+ webview.settings.javaScriptEnabled = true
+ webview.settings.domStorageEnabled = true
+ webview.addJavascriptInterface(
+ NativeWebApi(context = context, webView = webview, permissionLauncher),
+ "NativeApi"
+ )
+ webview.setDownloadListener { url, userAgent, _, mimetype, _ ->
+ if (!listOf("http://", "https://").any(url::startsWith)) {
+ webview.evaluateJavascript(
+ "alert('${
+ ResourcesUtils.getString(
+ context = context,
+ resId = R.string.core_can_only_download_http_https,
+ url
+ )
+ }')"
+ ) {}
+ return@setDownloadListener
+ }
+ val request = DownloadManager.Request(Uri.parse(url)).apply {
+ addRequestHeader("User-Agent", userAgent)
+ setMimeType(mimetype)
+ setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
+ }
+ val downloadManager: DownloadManager =
+ context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+ downloadManager.enqueue(request)
+ }
+}
+
+@Composable
+private fun rememberChromeClient(
+ context: Context,
+ fileChooserLauncher: ManagedActivityResultLauncher,
+ processCallback: (ValueCallback>?) -> Unit
+) = remember {
+ object : AccompanistWebChromeClient() {
+ override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
+ consoleMessage?.let {
+ Timber.tag("WebView").log(
+ priority = when (it.messageLevel()) {
+ ConsoleMessage.MessageLevel.TIP -> Log.VERBOSE
+ ConsoleMessage.MessageLevel.LOG -> Log.INFO
+ ConsoleMessage.MessageLevel.WARNING -> Log.WARN
+ ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
+ else -> Log.DEBUG
+ },
+ message = "${it.message()} (${it.lineNumber()})"
+ )
+ return true
+ }
+ return false
+ }
+
+ override fun onShowFileChooser(
+ webView: WebView?,
+ filePathCallback: ValueCallback>?,
+ fileChooserParams: FileChooserParams?
+ ): Boolean {
+ processCallback(filePathCallback)
+ val intent = fileChooserParams?.createIntent()
+ ?: Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "*/*"
+ }
+ fileChooserLauncher.launch(
+ Intent.createChooser(
+ intent,
+ fileChooserParams?.title ?: ResourcesUtils.getString(
+ context = context,
+ resId = R.string.core_file_select_one_text
+ )
+ )
+ )
+
+ return true
+ }
+ }
+}
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreenViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreenViewModel.kt
index abaad4e..8260653 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreenViewModel.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreenViewModel.kt
@@ -1,9 +1,12 @@
package top.fatweb.oxygen.toolbox.ui.view
+import android.content.Context
+import android.webkit.WebView
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
@@ -27,6 +30,7 @@ import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class ToolViewScreenViewModel @Inject constructor(
+ @ApplicationContext context: Context,
storeRepository: StoreRepository,
toolRepository: ToolRepository,
savedStateHandle: SavedStateHandle
@@ -55,6 +59,15 @@ class ToolViewScreenViewModel @Inject constructor(
initialValue = ToolViewUiState.Loading,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
)
+
+ val webviewInstance = flow {
+ val webviewInstance = WebView(context)
+ emit(WebViewInstanceState.Success(webviewInstance))
+ }.stateIn(
+ viewModelScope,
+ initialValue = WebViewInstanceState.Loading,
+ started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
+ )
}
private fun toolViewUiState(
@@ -141,12 +154,22 @@ sealed interface ToolViewUiState {
data object Loading : ToolViewUiState
}
+sealed interface WebViewInstanceState {
+ data class Success(
+ val webView: WebView
+ ) : WebViewInstanceState
+
+ data object Loading : WebViewInstanceState
+}
+
@OptIn(ExperimentalEncodingApi::class)
private fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
val dist = Base64.decodeToStringWithZip(distBase64)
val base = Base64.decodeToStringWithZip(baseBase64)
- return toolViewTemplate.replace("{{replace_code}}", "$dist\n$base")
+ return toolViewTemplate
+ .replace(oldValue = "{{replace_dict_code}}", newValue = dist)
+ .replace(oldValue = "{{replace_base_code}}", newValue = base)
}
private const val IS_PREVIEW = "IS_PREVIEW"