From 56892ffddd430c39c62f2f3ec66cc48b2615de15 Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:00:37 -0500 Subject: [PATCH 1/3] Remove unused webview functions --- .../app/ui/articles/detail/ArticleReader.kt | 11 +- .../app/ui/articles/detail/ArticleView.kt | 10 +- .../capyreader/app/ui/components/WebView.kt | 133 +----------------- 3 files changed, 14 insertions(+), 140 deletions(-) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt index 6139cdfa..798d0f59 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -35,12 +36,12 @@ fun ArticleReader( val colors = articleTemplateColors() var lastScrollY by rememberSaveable { mutableIntStateOf(0) } val webViewState = rememberWebViewState() - val byline = article.byline(context = LocalContext.current) + val context = LocalContext.current - fun render(): String { - return renderer.render( + val render by rememberUpdatedState { + renderer.render( article, - byline = byline, + byline = article.byline(context = context), colors = colors ) } @@ -64,7 +65,7 @@ fun ArticleReader( } } - LaunchedEffect(article.content) { + LaunchedEffect(article.id, article.content) { webViewState.loadHtml(render()) } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt index c2f34c9a..0750d79a 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt @@ -92,12 +92,10 @@ fun ArticleView( onRequestPrevious = onRequestPrevious, articles = articles, ) { - key(article.id) { - ArticleReader( - article = article, - scrollState = scrollState - ) - } + ArticleReader( + article = article, + scrollState = scrollState + ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt index 13b31ead..cb3e5ee1 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt @@ -2,25 +2,21 @@ package com.capyreader.app.ui.components import android.annotation.SuppressLint import android.graphics.Bitmap -import android.view.ViewGroup import android.view.ViewGroup.* import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView @@ -34,15 +30,6 @@ import coil.request.ImageRequest import com.capyreader.app.common.AppPreferences import com.capyreader.app.common.WebViewInterface import com.capyreader.app.common.openLink -import com.capyreader.app.ui.components.LoadingState.Finished -import com.capyreader.app.ui.components.LoadingState.Loading -import com.jocmp.capy.Article -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.ByteArrayInputStream @@ -62,28 +49,6 @@ import java.io.InputStream */ private const val ASSET_BASE_URL = "https://appassets.androidplatform.net" -/** - * A wrapper around the Android View WebView to provide a basic WebView composable. - * - * If you require more customisation you are most likely better rolling your own and using this - * wrapper as an example. - * - * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it - * is incorrectly sizing, use the layoutParams composable function instead. - * - * @param state The webview state holder where the Uri to load is defined. - * @param modifier A compose modifier - * @param navigator An optional navigator object that can be used to control the WebView's - * navigation from outside the composable. - * @param onCreated Called when the WebView is first created, this can be used to set additional - * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be - * subsequently overwritten after this lambda is called. - * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved - * if you need to save and restore state in this WebView. - * @param client Provides access to WebViewClient via subclassing - * @param chromeClient Provides access to WebChromeClient via subclassing - * @param factory An optional WebView factory for using a custom subclass of WebView - */ @SuppressLint("SetJavaScriptEnabled") @Composable fun WebView( @@ -106,6 +71,9 @@ fun WebView( client.state = state AndroidView( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), factory = { ctx -> WebView(ctx).apply { this.settings.javaScriptEnabled = true @@ -144,15 +112,10 @@ class AccompanistWebViewClient( override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - state.loadingState = Loading(0.0f) - state.errorsForCurrentRequest.clear() - state.pageTitle = null - state.pageIcon = null } override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) - state.loadingState = Finished } override fun shouldInterceptRequest( @@ -193,83 +156,10 @@ class AccompanistWebViewClient( return true } - - override fun onReceivedError( - view: WebView, - request: WebResourceRequest?, - error: WebResourceError? - ) { - super.onReceivedError(view, request, error) - - if (error != null) { - state.errorsForCurrentRequest.add(WebViewError(request, error)) - } - } -} - -/** - * Sealed class for constraining possible loading states. - * See [Loading] and [Finished]. - */ -sealed class LoadingState { - /** - * Describes a WebView that has not yet loaded for the first time. - */ - data object Initializing : LoadingState() - - /** - * Describes a webview between `onPageStarted` and `onPageFinished` events, contains a - * [progress] property which is updated by the webview. - */ - data class Loading(val progress: Float) : LoadingState() - - /** - * Describes a webview that has finished loading content. - */ - data object Finished : LoadingState() } -/** - * A state holder to hold the state for the WebView. In most cases this will be remembered - * using the rememberWebViewState(uri) function. - */ -@SuppressLint("SetJavaScriptEnabled") @Stable class WebViewState { - /** - * Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with - * progress) or the data loading has [LoadingState.Finished]. See [LoadingState] - */ - public var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing) - internal set - - /** - * Whether the webview is currently loading data in its main frame - */ - val isLoading: Boolean - get() = loadingState !is Finished - - /** - * The title received from the loaded content of the current page - */ - var pageTitle: String? by mutableStateOf(null) - internal set - - /** - * the favicon received from the loaded content of the current page - */ - var pageIcon: Bitmap? by mutableStateOf(null) - internal set - - /** - * A list for errors captured in the last load. Reset when a new page is loaded. - * Errors could be from any resource (iframe, image, etc.), not just for the main page. - * For more fine grained control use the OnError callback of the WebView. - */ - val errorsForCurrentRequest: SnapshotStateList = mutableStateListOf() - - // We need access to this in the state saver. An internal DisposableEffect or AndroidView - // onDestroy is called after the state saver and so can't be used. internal var webView by mutableStateOf(null) fun loadHtml(html: String) { @@ -283,21 +173,6 @@ class WebViewState { } } -/** - * A wrapper class to hold errors from the WebView. - */ -@Immutable -data class WebViewError( - /** - * The request the error came from. - */ - val request: WebResourceRequest?, - /** - * The error that was reported. - */ - val error: WebResourceError -) - @Composable fun rememberWebViewState() = remember { WebViewState() From e126a973285847124ab9639cc6aada1fb7377059 Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:22:51 -0500 Subject: [PATCH 2/3] Attempt to load articles immediately --- .../app/ui/articles/detail/ArticleReader.kt | 21 +++---- .../app/ui/articles/detail/ArticleView.kt | 2 +- .../capyreader/app/ui/components/WebView.kt | 58 ++++++++++++++----- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt index 798d0f59..c18c79c6 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -22,6 +23,7 @@ import com.capyreader.app.ui.components.WebView import com.capyreader.app.ui.components.rememberWebViewState import com.jocmp.capy.Article import com.jocmp.capy.articles.ArticleRenderer +import kotlinx.coroutines.launch import my.nanihadesuka.compose.ColumnScrollbar import my.nanihadesuka.compose.ScrollbarSettings import org.koin.compose.koinInject @@ -30,21 +32,11 @@ import org.koin.compose.koinInject fun ArticleReader( article: Article, scrollState: ScrollState, - renderer: ArticleRenderer = koinInject(), ) { + val scope = rememberCoroutineScope() val mediaViewer = LocalMediaViewer.current - val colors = articleTemplateColors() var lastScrollY by rememberSaveable { mutableIntStateOf(0) } val webViewState = rememberWebViewState() - val context = LocalContext.current - - val render by rememberUpdatedState { - renderer.render( - article, - byline = article.byline(context = context), - colors = colors - ) - } Scrollbar(scrollState = scrollState) { Column( @@ -57,6 +49,11 @@ fun ArticleReader( onNavigateToMedia = { mediaViewer.open(it) }, + onPageStarted = { + scope.launch { + scrollState.scrollTo(0) + } + }, onDispose = { lastScrollY = scrollState.value }, @@ -66,7 +63,7 @@ fun ArticleReader( } LaunchedEffect(article.id, article.content) { - webViewState.loadHtml(render()) + webViewState.loadHtml(article) } LaunchedEffect(lastScrollY, scrollState.maxValue) { diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt index 0750d79a..53b502b4 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt @@ -48,7 +48,7 @@ fun ArticleView( onRequestArticle: (id: String) -> Unit ) { val fullContent = LocalFullContent.current - val scrollState = rememberSaveable(article.id, key = article.id, saver = ScrollState.Saver) { + val scrollState = rememberSaveable(key = article.id, saver = ScrollState.Saver) { ScrollState(initial = 0) } diff --git a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt index cb3e5ee1..a7f9fe68 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt @@ -2,8 +2,8 @@ package com.capyreader.app.ui.components import android.annotation.SuppressLint import android.graphics.Bitmap -import android.view.ViewGroup.* -import android.webkit.WebResourceError +import android.util.Log +import android.view.View import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView @@ -11,7 +11,6 @@ import android.webkit.WebViewClient import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,6 +29,11 @@ import coil.request.ImageRequest import com.capyreader.app.common.AppPreferences import com.capyreader.app.common.WebViewInterface import com.capyreader.app.common.openLink +import com.capyreader.app.ui.articles.detail.articleTemplateColors +import com.capyreader.app.ui.articles.detail.byline +import com.jocmp.capy.Article +import com.jocmp.capy.articles.ArticleRenderer +import org.koin.compose.koinInject import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.ByteArrayInputStream @@ -54,6 +58,7 @@ private const val ASSET_BASE_URL = "https://appassets.androidplatform.net" fun WebView( state: WebViewState, onNavigateToMedia: (url: String) -> Unit, + onPageStarted: () -> Unit, onDispose: (WebView) -> Unit, ) { val context = LocalContext.current @@ -65,7 +70,8 @@ fun WebView( .setDomain("appassets.androidplatform.net") .addPathHandler("/assets/", AssetsPathHandler(context)) .addPathHandler("/res/", ResourcesPathHandler(context)) - .build() + .build(), + onPageStarted = onPageStarted, ) } client.state = state @@ -103,6 +109,7 @@ fun WebView( class AccompanistWebViewClient( private val assetLoader: WebViewAssetLoader, + private val onPageStarted: () -> Unit, ) : WebViewClient(), KoinComponent { lateinit var state: WebViewState @@ -112,6 +119,13 @@ class AccompanistWebViewClient( override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) + + view.postVisualStateCallback(1200L, object : WebView.VisualStateCallback() { + override fun onComplete(requestId: Long) { + onPageStarted() + view.visibility = View.VISIBLE + } + }) } override fun onPageFinished(view: WebView, url: String?) { @@ -136,7 +150,7 @@ class AccompanistWebViewClient( return WebResourceResponse( "image/jpg", "UTF-8", - bitmapInputStream(bitmap, Bitmap.CompressFormat.JPEG) + jpegStream(bitmap) ) } } catch (exception: Exception) { @@ -159,31 +173,47 @@ class AccompanistWebViewClient( } @Stable -class WebViewState { +class WebViewState( + private val renderer: ArticleRenderer, + private val colors: Map +) { internal var webView by mutableStateOf(null) - fun loadHtml(html: String) { - webView?.loadDataWithBaseURL( + fun loadHtml(article: Article) { + val view = webView ?: return + + view.visibility = View.INVISIBLE + + val html = renderer.render( + article, + byline = article.byline(context = view.context), + colors = colors + ) + + view.loadDataWithBaseURL( ASSET_BASE_URL, html, null, "UTF-8", - null, + null ) } } @Composable -fun rememberWebViewState() = remember { - WebViewState() +fun rememberWebViewState(renderer: ArticleRenderer = koinInject()): WebViewState { + val colors = articleTemplateColors() + + return remember { + WebViewState(renderer, colors) + } } -private fun bitmapInputStream( +private fun jpegStream( bitmap: Bitmap, - compressFormat: Bitmap.CompressFormat ): InputStream { val byteArrayOutputStream = ByteArrayOutputStream() - bitmap.compress(compressFormat, 100, byteArrayOutputStream) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) val bitmapData = byteArrayOutputStream.toByteArray() return ByteArrayInputStream(bitmapData) } From f619bc81224b7380b1c899f80b6b622b5101c8ff Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:48:23 -0500 Subject: [PATCH 3/3] Only make webview invisible on change --- .../capyreader/app/ui/components/WebView.kt | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt index a7f9fe68..d4483d61 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt @@ -2,7 +2,6 @@ package com.capyreader.app.ui.components import android.annotation.SuppressLint import android.graphics.Bitmap -import android.util.Log import android.view.View import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse @@ -15,6 +14,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -33,6 +33,10 @@ import com.capyreader.app.ui.articles.detail.articleTemplateColors import com.capyreader.app.ui.articles.detail.byline import com.jocmp.capy.Article import com.jocmp.capy.articles.ArticleRenderer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.compose.koinInject import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -84,6 +88,7 @@ fun WebView( WebView(ctx).apply { this.settings.javaScriptEnabled = true this.settings.mediaPlaybackRequiresUserGesture = false + this.settings.offscreenPreRaster = true isVerticalScrollBarEnabled = false isHorizontalScrollBarEnabled = false @@ -120,7 +125,7 @@ class AccompanistWebViewClient( override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - view.postVisualStateCallback(1200L, object : WebView.VisualStateCallback() { + view.postVisualStateCallback(requestId, object : WebView.VisualStateCallback() { override fun onComplete(requestId: Long) { onPageStarted() view.visibility = View.VISIBLE @@ -128,6 +133,8 @@ class AccompanistWebViewClient( }) } + private val requestId = 1200L + override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) } @@ -175,37 +182,52 @@ class AccompanistWebViewClient( @Stable class WebViewState( private val renderer: ArticleRenderer, - private val colors: Map + private val colors: Map, + private val scope: CoroutineScope, ) { internal var webView by mutableStateOf(null) + private var htmlId: Long? = null + fun loadHtml(article: Article) { + val id = article.id.hashCode().toLong() val view = webView ?: return - view.visibility = View.INVISIBLE + scope.launch { + if (id != htmlId) { + view.visibility = View.INVISIBLE + } - val html = renderer.render( - article, - byline = article.byline(context = view.context), - colors = colors - ) + htmlId = id - view.loadDataWithBaseURL( - ASSET_BASE_URL, - html, - null, - "UTF-8", - null - ) + withContext(Dispatchers.IO) { + val html = renderer.render( + article, + byline = article.byline(context = view.context), + colors = colors + ) + + withContext(Dispatchers.Main) { + view.loadDataWithBaseURL( + ASSET_BASE_URL, + html, + null, + "UTF-8", + null + ) + } + } + } } } @Composable fun rememberWebViewState(renderer: ArticleRenderer = koinInject()): WebViewState { val colors = articleTemplateColors() + val scope = rememberCoroutineScope() return remember { - WebViewState(renderer, colors) + WebViewState(renderer, colors, scope) } }