diff options
Diffstat (limited to 'app/src/main/java/org')
4 files changed, 108 insertions, 24 deletions
diff --git a/app/src/main/java/org/terst/feedreader/data/Settings.kt b/app/src/main/java/org/terst/feedreader/data/Settings.kt index be3e809..ebeba6c 100644 --- a/app/src/main/java/org/terst/feedreader/data/Settings.kt +++ b/app/src/main/java/org/terst/feedreader/data/Settings.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +import kotlin.math.roundToInt val Context.settingsDataStore by preferencesDataStore(name = "settings") @@ -38,6 +39,12 @@ private const val READER_FONT_SIZE_MIN_PX = 14 private const val READER_FONT_SIZE_MAX_PX = 28 val READER_FONT_SIZE_RANGE_PX = READER_FONT_SIZE_MIN_PX..READER_FONT_SIZE_MAX_PX +// Pure so the pinch-to-zoom math is testable without a WebView/gesture detector in the loop -- +// ReaderWebView.kt calls this once per gesture (at pinch-end, with the cumulative scale factor +// accumulated across the whole gesture), not per touch-move frame. +fun pinchZoomFontSizePx(basePx: Int, scaleFactor: Float): Int = + (basePx * scaleFactor).roundToInt().coerceIn(READER_FONT_SIZE_RANGE_PX.first, READER_FONT_SIZE_RANGE_PX.last) + /** Stash connection settings, configured on the Settings screen. Both blank until set. */ class Settings(private val context: Context) { val stashBaseUrl: Flow<String> = diff --git a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt index 0f7540d..d71b020 100644 --- a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt @@ -234,9 +234,11 @@ fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Un factory = { ctx -> newReaderWebView( ctx, - onOpenLink, - onLongPressLink = { longPressedLink = it }, - onYouTubeVideo = { youtubeVideoId = it } + ReaderWebViewCallbacks( + onOpenLink = onOpenLink, + onLongPressLink = { longPressedLink = it }, + onYouTubeVideo = { youtubeVideoId = it } + ) ).apply { loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null) loadedContent.value = currentExtractedContent diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index a89052c..e2b8aa9 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -180,6 +181,10 @@ private fun ReaderContent( ) { val context = LocalContext.current val db = remember { AppDatabase.get(context) } + // Independently remembered rather than threaded down from ReaderScreen, matching db/scope + // above -- Settings is a stateless DataStore wrapper, cheap to construct again, and this + // keeps pinch-to-zoom from growing ReaderContent's already-long parameter list any further. + val settings = remember { Settings(context) } val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } @@ -526,19 +531,28 @@ private fun ReaderContent( // changing it in Settings and coming back to an already-open article // picks up the new size, without reloading on every unrelated recomposition. val loadedKey = remember(articleId) { mutableStateOf<Pair<String?, Int>?>(null) } + // newReaderWebView's factory runs once per WebView instance, but fontSizePx can + // change later (e.g. the user tweaks it in Settings while this same article stays + // open) -- rememberUpdatedState keeps the pinch gesture's "what am I scaling from" + // read current instead of frozen at whatever it was on first composition. + val latestFontSizePx by rememberUpdatedState(fontSizePx) AndroidView( modifier = Modifier.fillMaxSize(), factory = { ctx -> newReaderWebView( ctx, - onOpenLink, - onLongPressLink = { longPressedLink = it }, - onYouTubeVideo = { youtubeVideoId = it }, - onScrollChanged = { scrollY -> ReaderScrollPositions.set(articleId, scrollY) }, - // The page's real content height isn't known until layout finishes - // -- scrolling to a saved offset any earlier would just clamp back - // to 0 on a page that hasn't grown tall enough yet. - onPageFinished = { webView -> webView.scrollTo(0, ReaderScrollPositions.get(articleId)) } + ReaderWebViewCallbacks( + onOpenLink = onOpenLink, + onLongPressLink = { longPressedLink = it }, + onYouTubeVideo = { youtubeVideoId = it }, + onScrollChanged = { scrollY -> ReaderScrollPositions.set(articleId, scrollY) }, + // The page's real content height isn't known until layout finishes + // -- scrolling to a saved offset any earlier would just clamp back + // to 0 on a page that hasn't grown tall enough yet. + onPageFinished = { webView -> webView.scrollTo(0, ReaderScrollPositions.get(articleId)) }, + currentFontSizePx = { latestFontSizePx }, + onFontSizeChanged = { newSizePx -> scope.launch { settings.setReaderFontSizePx(newSizePx) } } + ) ).apply { val html = wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl, fontSizePx) loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null) diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt index b13fc58..039255f 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt @@ -3,6 +3,7 @@ package org.terst.feedreader.ui import android.content.Context import android.content.Intent import android.net.Uri +import android.view.ScaleGestureDetector import android.view.View import android.webkit.WebChromeClient import android.webkit.WebResourceRequest @@ -23,6 +24,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import org.terst.feedreader.data.DEFAULT_READER_FONT_SIZE_PX +import org.terst.feedreader.data.pinchZoomFontSizePx // In-memory only (process lifetime, not persisted to disk) -- scoped to "don't lose my // place if Next/Previous takes me away and back," not to surviving an app restart. A @@ -76,15 +78,44 @@ private fun extractYouTubeVideoId(uri: Uri): String? { * YouTubePlayerDialog instead, so watching a video doesn't leave the reader. Falls * back to the external-open behavior when a confident video ID can't be extracted * (a playlist/channel link, say). + * + * Pinch-to-zoom adjusts the persisted reader font size, not WebView's own page zoom + * (never enabled here -- setSupportZoom/builtInZoomControls are left at their defaults, + * which is effectively off). A real page zoom would scale layout and images along with + * text and wouldn't survive the next article/reload; this app already has a font-size + * concept the rest of the reader respects (Settings.readerFontSizePx, adjustable from + * the Settings screen), so pinching should drive that same value instead of inventing a + * second, WebView-local notion of "how big does this look right now." + * + * Getting this right without breaking ordinary one-finger scrolling/tapping is the whole + * trick: a ScaleGestureDetector is fed every touch via setOnTouchListener, but that + * listener always returns false (never consumes the event), so WebView's own touch + * handling -- scrolling, link taps, the long-press-link listener above -- proceeds + * exactly as before. ScaleGestureDetector only calls back once a second pointer is down, + * so a single-finger drag never triggers any of this. + * + * currentFontSizePx is read fresh (not captured once) when a gesture ends, since the + * WebView this attaches to is created once but can outlive a Settings-screen-driven font + * size change made while this same article is still open. + * + * During the gesture the WebView is visually scaled live (View.scaleX/scaleY) for + * immediate feedback -- cheap, and independent of the WebView's own content layout. + * At gesture end the scale resets to 1x and onFontSizeChanged reports the new, + * clamped, rounded font size for the caller to persist; ReaderScreen's existing + * content-vs-fontSizePx reload logic (see ReaderScreen.ReaderContent) then re-renders + * the actual HTML at that size. */ -fun newReaderWebView( - context: Context, - onOpenLink: (String) -> Unit, - onLongPressLink: (String) -> Unit = {}, - onYouTubeVideo: (String) -> Unit = {}, - onScrollChanged: (Int) -> Unit = {}, - onPageFinished: (WebView) -> Unit = {} -): WebView = +data class ReaderWebViewCallbacks( + val onOpenLink: (String) -> Unit, + val onLongPressLink: (String) -> Unit = {}, + val onYouTubeVideo: (String) -> Unit = {}, + val onScrollChanged: (Int) -> Unit = {}, + val onPageFinished: (WebView) -> Unit = {}, + val currentFontSizePx: () -> Int = { DEFAULT_READER_FONT_SIZE_PX }, + val onFontSizeChanged: (Int) -> Unit = {} +) + +fun newReaderWebView(context: Context, callbacks: ReaderWebViewCallbacks): WebView = WebView(context).apply { settings.javaScriptEnabled = false overScrollMode = View.OVER_SCROLL_NEVER @@ -95,11 +126,41 @@ fun newReaderWebView( // Next/Previous replaces this screen's back-stack entry with a fresh one for the same // article, which recreates the WebView from scratch and would otherwise always land // back at the top. - setOnScrollChangeListener { _, _, scrollY, _, _ -> onScrollChanged(scrollY) } + setOnScrollChangeListener { _, _, scrollY, _, _ -> callbacks.onScrollChanged(scrollY) } + + var pinchScale = 1f + val webView = this + val scaleDetector = ScaleGestureDetector( + context, + object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + pinchScale = 1f + return true + } + + override fun onScale(detector: ScaleGestureDetector): Boolean { + pinchScale *= detector.scaleFactor + webView.scaleX = pinchScale + webView.scaleY = pinchScale + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) { + val newSizePx = pinchZoomFontSizePx(callbacks.currentFontSizePx(), pinchScale) + webView.scaleX = 1f + webView.scaleY = 1f + callbacks.onFontSizeChanged(newSizePx) + } + } + ) + setOnTouchListener { _, event -> + scaleDetector.onTouchEvent(event) + false // never consume -- WebView still handles scroll/tap/long-press normally + } webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) - onPageFinished(view) + callbacks.onPageFinished(view) } override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { @@ -117,12 +178,12 @@ fun newReaderWebView( if (isYouTubeWatchUrl(url)) { val videoId = extractYouTubeVideoId(url) if (videoId != null) { - onYouTubeVideo(videoId) + callbacks.onYouTubeVideo(videoId) } else { context.startActivity(Intent(Intent.ACTION_VIEW, url)) } } else { - onOpenLink(url.toString()) + callbacks.onOpenLink(url.toString()) } return true } @@ -134,7 +195,7 @@ fun newReaderWebView( else -> null } if (url != null) { - onLongPressLink(url) + callbacks.onLongPressLink(url) true } else { false |
