feat: possibility to open URLs in internal WebView
This commit is contained in:
parent
c13ef232bd
commit
275fd50f76
@ -0,0 +1,55 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
@Composable
|
||||||
|
actual fun CustomWebView(
|
||||||
|
navigator: WebViewNavigator,
|
||||||
|
modifier: Modifier,
|
||||||
|
url: String,
|
||||||
|
) {
|
||||||
|
var webView: WebView? = null
|
||||||
|
|
||||||
|
LaunchedEffect(true) {
|
||||||
|
navigator.events.onEach {
|
||||||
|
when (it) {
|
||||||
|
WebViewNavigationEvent.GoBack -> webView?.goBack()
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = { context ->
|
||||||
|
WebView(context).apply {
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||||
|
navigator.canGoBack = view.canGoBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
|
||||||
|
loadUrl(url)
|
||||||
|
webView = this
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = {
|
||||||
|
webView = it
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun CustomWebView(
|
||||||
|
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
url: String,
|
||||||
|
)
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
internal sealed interface WebViewNavigationEvent {
|
||||||
|
object GoBack : WebViewNavigationEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebViewNavigator(
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
var canGoBack: Boolean = true
|
||||||
|
internal val events = MutableSharedFlow<WebViewNavigationEvent>()
|
||||||
|
|
||||||
|
fun goBack() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
events.emit(WebViewNavigationEvent.GoBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberWebViewNavigator(): WebViewNavigator {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
return remember {
|
||||||
|
WebViewNavigator(
|
||||||
|
coroutineScope = scope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -2,16 +2,26 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
|||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.web.WebViewScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.Markdown
|
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.Markdown
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownColor
|
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownColor
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownTypography
|
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownTypography
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.di.getTemporaryKeyStore
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostCardBody(
|
fun PostCardBody(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String,
|
||||||
) {
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
val keyStore = remember { getTemporaryKeyStore() }
|
||||||
|
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
Markdown(
|
Markdown(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@ -30,6 +40,14 @@ fun PostCardBody(
|
|||||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
backgroundCode = MaterialTheme.colorScheme.surfaceVariant,
|
backgroundCode = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
),
|
),
|
||||||
|
onOpenUrl = { url ->
|
||||||
|
val openExternal = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false]
|
||||||
|
if (openExternal) {
|
||||||
|
uriHandler.openUri(url)
|
||||||
|
} else {
|
||||||
|
navigator?.push(WebViewScreen(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,26 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
|||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.web.WebViewScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.Markdown
|
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.Markdown
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownColor
|
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownColor
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownTypography
|
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.model.markdownTypography
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.di.getTemporaryKeyStore
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostCardTitle(
|
fun PostCardTitle(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
val keyStore = remember { getTemporaryKeyStore() }
|
||||||
|
|
||||||
Markdown(
|
Markdown(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
content = text,
|
content = text,
|
||||||
@ -22,5 +32,13 @@ fun PostCardTitle(
|
|||||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
backgroundCode = MaterialTheme.colorScheme.surfaceVariant,
|
backgroundCode = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
),
|
),
|
||||||
|
onOpenUrl = { url ->
|
||||||
|
val openExternal = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false]
|
||||||
|
if (openExternal) {
|
||||||
|
uriHandler.openUri(url)
|
||||||
|
} else {
|
||||||
|
navigator?.push(WebViewScreen(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
@ -18,6 +19,10 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.web.WebViewScreen
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.di.getTemporaryKeyStore
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostLinkBanner(
|
fun PostLinkBanner(
|
||||||
@ -25,6 +30,9 @@ fun PostLinkBanner(
|
|||||||
url: String,
|
url: String,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
val keyStore = remember { getTemporaryKeyStore() }
|
||||||
|
|
||||||
if (url.isNotEmpty()) {
|
if (url.isNotEmpty()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -32,7 +40,12 @@ fun PostLinkBanner(
|
|||||||
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f),
|
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f),
|
||||||
shape = RoundedCornerShape(CornerSize.l),
|
shape = RoundedCornerShape(CornerSize.l),
|
||||||
).onClick {
|
).onClick {
|
||||||
|
val openExternal = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false]
|
||||||
|
if (openExternal) {
|
||||||
uriHandler.openUri(url)
|
uriHandler.openUri(url)
|
||||||
|
} else {
|
||||||
|
navigator?.push(WebViewScreen(url))
|
||||||
|
}
|
||||||
}.padding(
|
}.padding(
|
||||||
horizontal = Spacing.m,
|
horizontal = Spacing.m,
|
||||||
vertical = Spacing.s,
|
vertical = Spacing.s,
|
||||||
|
@ -16,6 +16,7 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator {
|
|||||||
private var navigator: Navigator? = null
|
private var navigator: Navigator? = null
|
||||||
private var currentTab: Tab? = null
|
private var currentTab: Tab? = null
|
||||||
private val scope = CoroutineScope(SupervisorJob())
|
private val scope = CoroutineScope(SupervisorJob())
|
||||||
|
private var canGoBackCallback: (() -> Boolean)? = null
|
||||||
|
|
||||||
override fun setRootNavigator(value: Navigator?) {
|
override fun setRootNavigator(value: Navigator?) {
|
||||||
navigator = value
|
navigator = value
|
||||||
@ -38,4 +39,10 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setCanGoBackCallback(value: (() -> Boolean)?) {
|
||||||
|
canGoBackCallback = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCanGoBackCallback(): (() -> Boolean)? = canGoBackCallback
|
||||||
}
|
}
|
@ -13,6 +13,10 @@ interface NavigationCoordinator {
|
|||||||
|
|
||||||
fun setRootNavigator(value: Navigator?)
|
fun setRootNavigator(value: Navigator?)
|
||||||
|
|
||||||
|
fun setCanGoBackCallback(value: (() -> Boolean)?)
|
||||||
|
|
||||||
|
fun getCanGoBackCallback(): (() -> Boolean)?
|
||||||
|
|
||||||
fun getRootNavigator(): Navigator?
|
fun getRootNavigator(): Navigator?
|
||||||
|
|
||||||
fun setBottomBarScrollConnection(value: NestedScrollConnection?)
|
fun setBottomBarScrollConnection(value: NestedScrollConnection?)
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.web
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomWebView
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.rememberWebViewNavigator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||||
|
|
||||||
|
class WebViewScreen(
|
||||||
|
private val url: String,
|
||||||
|
) : Screen {
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {},
|
||||||
|
navigationIcon = {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.onClick {
|
||||||
|
navigator?.pop()
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
val navigationCoordinator = remember { getNavigationCoordinator() }
|
||||||
|
val webNavigator = rememberWebViewNavigator()
|
||||||
|
|
||||||
|
DisposableEffect(key) {
|
||||||
|
navigationCoordinator.setCanGoBackCallback {
|
||||||
|
val result = webNavigator.canGoBack
|
||||||
|
if (result) {
|
||||||
|
webNavigator.goBack()
|
||||||
|
return@setCanGoBackCallback false
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
onDispose {
|
||||||
|
navigationCoordinator.setCanGoBackCallback(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomWebView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
navigator = webNavigator,
|
||||||
|
url = url,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.interop.UIKitView
|
||||||
|
import kotlinx.cinterop.readValue
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import platform.CoreGraphics.CGRectZero
|
||||||
|
import platform.WebKit.WKNavigation
|
||||||
|
import platform.WebKit.WKNavigationDelegateProtocol
|
||||||
|
import platform.WebKit.WKWebView
|
||||||
|
import platform.WebKit.WKWebViewConfiguration
|
||||||
|
import platform.darwin.NSObject
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun CustomWebView(
|
||||||
|
navigator: WebViewNavigator,
|
||||||
|
modifier: Modifier,
|
||||||
|
url: String,
|
||||||
|
) {
|
||||||
|
var webView: WKWebView? = null
|
||||||
|
|
||||||
|
LaunchedEffect(true) {
|
||||||
|
navigator.events.onEach {
|
||||||
|
when (it) {
|
||||||
|
WebViewNavigationEvent.GoBack -> webView?.goBack()
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIKitView(
|
||||||
|
factory = {
|
||||||
|
val config = WKWebViewConfiguration().apply {
|
||||||
|
allowsInlineMediaPlayback = true
|
||||||
|
}
|
||||||
|
WKWebView(
|
||||||
|
frame = CGRectZero.readValue(),
|
||||||
|
configuration = config
|
||||||
|
).apply {
|
||||||
|
userInteractionEnabled = true
|
||||||
|
allowsBackForwardNavigationGestures = true
|
||||||
|
val navigationDelegate = object : NSObject(), WKNavigationDelegateProtocol {
|
||||||
|
|
||||||
|
override fun webView(
|
||||||
|
webView: WKWebView,
|
||||||
|
didFinishNavigation: WKNavigation?,
|
||||||
|
) {
|
||||||
|
navigator.canGoBack = webView.canGoBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.navigationDelegate = navigationDelegate
|
||||||
|
}.also {
|
||||||
|
webView = it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
onRelease = {
|
||||||
|
webView = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -59,6 +59,7 @@ fun Markdown(
|
|||||||
padding: MarkdownPadding = markdownPadding(),
|
padding: MarkdownPadding = markdownPadding(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
flavour: MarkdownFlavourDescriptor = GFMFlavourDescriptor(),
|
flavour: MarkdownFlavourDescriptor = GFMFlavourDescriptor(),
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalReferenceLinkHandler provides ReferenceLinkHandlerImpl(),
|
LocalReferenceLinkHandler provides ReferenceLinkHandlerImpl(),
|
||||||
@ -69,9 +70,9 @@ fun Markdown(
|
|||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content)
|
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content)
|
||||||
parsedTree.children.forEach { node ->
|
parsedTree.children.forEach { node ->
|
||||||
if (!node.handleElement(content)) {
|
if (!node.handleElement(content, onOpenUrl)) {
|
||||||
node.children.forEach { child ->
|
node.children.forEach { child ->
|
||||||
child.handleElement(content)
|
child.handleElement(content, onOpenUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,12 +81,15 @@ fun Markdown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ASTNode.handleElement(content: String): Boolean {
|
private fun ASTNode.handleElement(
|
||||||
|
content: String,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
|
): Boolean {
|
||||||
val typography = LocalMarkdownTypography.current
|
val typography = LocalMarkdownTypography.current
|
||||||
var handled = true
|
var handled = true
|
||||||
Spacer(Modifier.height(LocalMarkdownPadding.current.block))
|
Spacer(Modifier.height(LocalMarkdownPadding.current.block))
|
||||||
when (type) {
|
when (type) {
|
||||||
TEXT -> MarkdownText(getTextInNode(content).toString())
|
TEXT -> MarkdownText(getTextInNode(content).toString(), onOpenUrl = onOpenUrl)
|
||||||
EOL -> {}
|
EOL -> {}
|
||||||
CODE_FENCE -> MarkdownCodeFence(content, this)
|
CODE_FENCE -> MarkdownCodeFence(content, this)
|
||||||
CODE_BLOCK -> MarkdownCodeBlock(content, this)
|
CODE_BLOCK -> MarkdownCodeBlock(content, this)
|
||||||
@ -96,13 +100,29 @@ private fun ASTNode.handleElement(content: String): Boolean {
|
|||||||
ATX_5 -> MarkdownHeader(content, this, typography.h5)
|
ATX_5 -> MarkdownHeader(content, this, typography.h5)
|
||||||
ATX_6 -> MarkdownHeader(content, this, typography.h6)
|
ATX_6 -> MarkdownHeader(content, this, typography.h6)
|
||||||
BLOCK_QUOTE -> MarkdownBlockQuote(content, this)
|
BLOCK_QUOTE -> MarkdownBlockQuote(content, this)
|
||||||
PARAGRAPH -> MarkdownParagraph(content, this, style = typography.paragraph)
|
PARAGRAPH -> MarkdownParagraph(
|
||||||
|
content,
|
||||||
|
this,
|
||||||
|
style = typography.paragraph,
|
||||||
|
onOpenUrl = onOpenUrl
|
||||||
|
)
|
||||||
|
|
||||||
ORDERED_LIST -> Column(modifier = Modifier) {
|
ORDERED_LIST -> Column(modifier = Modifier) {
|
||||||
MarkdownOrderedList(content, this@handleElement, style = typography.ordered)
|
MarkdownOrderedList(
|
||||||
|
content,
|
||||||
|
this@handleElement,
|
||||||
|
style = typography.ordered,
|
||||||
|
onOpenUrl = onOpenUrl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
UNORDERED_LIST -> Column(modifier = Modifier) {
|
UNORDERED_LIST -> Column(modifier = Modifier) {
|
||||||
MarkdownBulletList(content, this@handleElement, style = typography.bullet)
|
MarkdownBulletList(
|
||||||
|
content,
|
||||||
|
this@handleElement,
|
||||||
|
style = typography.bullet,
|
||||||
|
onOpenUrl = onOpenUrl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IMAGE -> MarkdownImage(content, this)
|
IMAGE -> MarkdownImage(content, this)
|
||||||
|
@ -65,6 +65,7 @@ internal fun MarkdownOrderedList(
|
|||||||
node: ASTNode,
|
node: ASTNode,
|
||||||
style: TextStyle = LocalMarkdownTypography.current.ordered,
|
style: TextStyle = LocalMarkdownTypography.current.ordered,
|
||||||
level: Int = 0,
|
level: Int = 0,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val orderedListHandler = LocalOrderedListHandler.current
|
val orderedListHandler = LocalOrderedListHandler.current
|
||||||
MarkdownListItems(content, node, style, level) { child ->
|
MarkdownListItems(content, node, style, level) { child ->
|
||||||
@ -81,7 +82,12 @@ internal fun MarkdownOrderedList(
|
|||||||
buildMarkdownAnnotatedString(content, child.children.filterNonListTypes())
|
buildMarkdownAnnotatedString(content, child.children.filterNonListTypes())
|
||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
MarkdownText(text, Modifier.padding(bottom = 4.dp), style = style)
|
MarkdownText(
|
||||||
|
text,
|
||||||
|
Modifier.padding(bottom = 4.dp),
|
||||||
|
style = style,
|
||||||
|
onOpenUrl = onOpenUrl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +98,7 @@ internal fun MarkdownBulletList(
|
|||||||
node: ASTNode,
|
node: ASTNode,
|
||||||
style: TextStyle = LocalMarkdownTypography.current.bullet,
|
style: TextStyle = LocalMarkdownTypography.current.bullet,
|
||||||
level: Int = 0,
|
level: Int = 0,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val bulletHandler = LocalBulletListHandler.current
|
val bulletHandler = LocalBulletListHandler.current
|
||||||
MarkdownListItems(content, node, style, level) { child ->
|
MarkdownListItems(content, node, style, level) { child ->
|
||||||
@ -108,7 +115,12 @@ internal fun MarkdownBulletList(
|
|||||||
buildMarkdownAnnotatedString(content, child.children.filterNonListTypes())
|
buildMarkdownAnnotatedString(content, child.children.filterNonListTypes())
|
||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
MarkdownText(text, Modifier.padding(bottom = 4.dp), style = style)
|
MarkdownText(
|
||||||
|
text,
|
||||||
|
Modifier.padding(bottom = 4.dp),
|
||||||
|
style = style,
|
||||||
|
onOpenUrl = onOpenUrl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,12 @@ internal fun MarkdownParagraph(
|
|||||||
content: String,
|
content: String,
|
||||||
node: ASTNode,
|
node: ASTNode,
|
||||||
style: TextStyle = LocalMarkdownTypography.current.paragraph,
|
style: TextStyle = LocalMarkdownTypography.current.paragraph,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val styledText = buildAnnotatedString {
|
val styledText = buildAnnotatedString {
|
||||||
pushStyle(style.toSpanStyle())
|
pushStyle(style.toSpanStyle())
|
||||||
buildMarkdownAnnotatedString(content, node)
|
buildMarkdownAnnotatedString(content, node)
|
||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
MarkdownText(styledText, style = style)
|
MarkdownText(styledText, style = style, onOpenUrl = onOpenUrl)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
@ -30,8 +29,9 @@ internal fun MarkdownText(
|
|||||||
content: String,
|
content: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
style: TextStyle = LocalMarkdownTypography.current.text,
|
style: TextStyle = LocalMarkdownTypography.current.text,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
MarkdownText(AnnotatedString(content), modifier, style)
|
MarkdownText(AnnotatedString(content), modifier, style, onOpenUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -39,8 +39,8 @@ internal fun MarkdownText(
|
|||||||
content: AnnotatedString,
|
content: AnnotatedString,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
style: TextStyle = LocalMarkdownTypography.current.text,
|
style: TextStyle = LocalMarkdownTypography.current.text,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val referenceLinkHandler = LocalReferenceLinkHandler.current
|
val referenceLinkHandler = LocalReferenceLinkHandler.current
|
||||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
|
||||||
@ -52,7 +52,10 @@ internal fun MarkdownText(
|
|||||||
val position = layoutResult.getOffsetForPosition(pos)
|
val position = layoutResult.getOffsetForPosition(pos)
|
||||||
content.getStringAnnotations(TAG_URL, position, position)
|
content.getStringAnnotations(TAG_URL, position, position)
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { uriHandler.openUri(referenceLinkHandler.find(it.item)) }
|
?.let {
|
||||||
|
val url = referenceLinkHandler.find(it.item)
|
||||||
|
onOpenUrl?.invoke(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,4 +13,5 @@ object KeyStoreKeys {
|
|||||||
const val BlurNsfw = "blurNsfw"
|
const val BlurNsfw = "blurNsfw"
|
||||||
const val NavItemTitlesVisible = "navItemTitlesVisible"
|
const val NavItemTitlesVisible = "navItemTitlesVisible"
|
||||||
const val DynamicColors = "dynamicColors"
|
const val DynamicColors = "dynamicColors"
|
||||||
|
const val OpenUrlsInExternalBrowser = "openUrlsInExternalBrowser"
|
||||||
}
|
}
|
||||||
|
@ -240,6 +240,19 @@ class SettingsScreen : Screen {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URL open
|
||||||
|
SettingsSwitchRow(
|
||||||
|
title = stringResource(MR.strings.settings_open_url_external),
|
||||||
|
value = uiState.openUrlsInExternalBrowser,
|
||||||
|
onValueChanged = { value ->
|
||||||
|
model.reduce(
|
||||||
|
SettingsScreenMviModel.Intent.ChangeOpenUrlsInExternalBrowser(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// NSFW options
|
// NSFW options
|
||||||
SettingsSwitchRow(
|
SettingsSwitchRow(
|
||||||
title = stringResource(MR.strings.settings_include_nsfw),
|
title = stringResource(MR.strings.settings_include_nsfw),
|
||||||
|
@ -20,6 +20,7 @@ interface SettingsScreenMviModel :
|
|||||||
data class ChangeDynamicColors(val value: Boolean) : Intent
|
data class ChangeDynamicColors(val value: Boolean) : Intent
|
||||||
data class ChangeIncludeNsfw(val value: Boolean) : Intent
|
data class ChangeIncludeNsfw(val value: Boolean) : Intent
|
||||||
data class ChangeBlurNsfw(val value: Boolean) : Intent
|
data class ChangeBlurNsfw(val value: Boolean) : Intent
|
||||||
|
data class ChangeOpenUrlsInExternalBrowser(val value: Boolean) : Intent
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UiState(
|
data class UiState(
|
||||||
@ -35,6 +36,7 @@ interface SettingsScreenMviModel :
|
|||||||
val dynamicColors: Boolean = false,
|
val dynamicColors: Boolean = false,
|
||||||
val includeNsfw: Boolean = true,
|
val includeNsfw: Boolean = true,
|
||||||
val blurNsfw: Boolean = true,
|
val blurNsfw: Boolean = true,
|
||||||
|
val openUrlsInExternalBrowser: Boolean = false,
|
||||||
val appVersion: String = "",
|
val appVersion: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,6 +82,7 @@ class SettingsScreenViewModel(
|
|||||||
val listingType = keyStore[KeyStoreKeys.DefaultListingType, 0].toListingType()
|
val listingType = keyStore[KeyStoreKeys.DefaultListingType, 0].toListingType()
|
||||||
val postSortType = keyStore[KeyStoreKeys.DefaultPostSortType, 0].toSortType()
|
val postSortType = keyStore[KeyStoreKeys.DefaultPostSortType, 0].toSortType()
|
||||||
val commentSortType = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType()
|
val commentSortType = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType()
|
||||||
|
val openUrlsInExternalBrowser = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false]
|
||||||
mvi.updateState {
|
mvi.updateState {
|
||||||
it.copy(
|
it.copy(
|
||||||
defaultListingType = listingType,
|
defaultListingType = listingType,
|
||||||
@ -89,8 +90,9 @@ class SettingsScreenViewModel(
|
|||||||
defaultCommentSortType = commentSortType,
|
defaultCommentSortType = commentSortType,
|
||||||
includeNsfw = keyStore[KeyStoreKeys.IncludeNsfw, true],
|
includeNsfw = keyStore[KeyStoreKeys.IncludeNsfw, true],
|
||||||
blurNsfw = keyStore[KeyStoreKeys.BlurNsfw, true],
|
blurNsfw = keyStore[KeyStoreKeys.BlurNsfw, true],
|
||||||
appVersion = AppInfo.versionCode,
|
|
||||||
supportsDynamicColors = colorSchemeProvider.supportsDynamicColors,
|
supportsDynamicColors = colorSchemeProvider.supportsDynamicColors,
|
||||||
|
openUrlsInExternalBrowser = openUrlsInExternalBrowser,
|
||||||
|
appVersion = AppInfo.versionCode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,6 +138,10 @@ class SettingsScreenViewModel(
|
|||||||
is SettingsScreenMviModel.Intent.ChangeDynamicColors -> {
|
is SettingsScreenMviModel.Intent.ChangeDynamicColors -> {
|
||||||
changeDynamicColors(intent.value)
|
changeDynamicColors(intent.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is SettingsScreenMviModel.Intent.ChangeOpenUrlsInExternalBrowser -> {
|
||||||
|
changeOpenUrlsInExternalBrowser(intent.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,4 +194,9 @@ class SettingsScreenViewModel(
|
|||||||
themeRepository.changeDynamicColors(value)
|
themeRepository.changeDynamicColors(value)
|
||||||
keyStore.save(KeyStoreKeys.DynamicColors, value)
|
keyStore.save(KeyStoreKeys.DynamicColors, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun changeOpenUrlsInExternalBrowser(value: Boolean) {
|
||||||
|
mvi.updateState { it.copy(openUrlsInExternalBrowser = value) }
|
||||||
|
keyStore.save(KeyStoreKeys.OpenUrlsInExternalBrowser, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
androidx_activity_compose = "1.7.2"
|
androidx_activity_compose = "1.7.2"
|
||||||
androidx_crypto = "1.0.0"
|
androidx_crypto = "1.0.0"
|
||||||
android_gradle = "7.4.2"
|
android_gradle = "7.4.2"
|
||||||
compose = "1.4.3"
|
compose = "1.5.1"
|
||||||
crashlytics = "18.4.1"
|
crashlytics = "18.4.1"
|
||||||
crashlytics_gradle = "2.9.9"
|
crashlytics_gradle = "2.9.9"
|
||||||
gms_gradle = "4.3.15"
|
gms_gradle = "4.3.15"
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
<string name="settings_blur_nsfw">Blur NSFW images</string>
|
<string name="settings_blur_nsfw">Blur NSFW images</string>
|
||||||
<string name="settings_app_version">App version</string>
|
<string name="settings_app_version">App version</string>
|
||||||
<string name="settings_dynamic_colors">Use dynamic colors</string>
|
<string name="settings_dynamic_colors">Use dynamic colors</string>
|
||||||
|
<string name="settings_open_url_external">Open URLs in external browser</string>
|
||||||
|
|
||||||
<string name="community_button_subscribe">Subscribe</string>
|
<string name="community_button_subscribe">Subscribe</string>
|
||||||
<string name="community_button_subscribed">Subscribed</string>
|
<string name="community_button_subscribed">Subscribed</string>
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
<string name="settings_blur_nsfw">Sfuma immagini NSFW</string>
|
<string name="settings_blur_nsfw">Sfuma immagini NSFW</string>
|
||||||
<string name="settings_app_version">Versione app</string>
|
<string name="settings_app_version">Versione app</string>
|
||||||
<string name="settings_dynamic_colors">Usa colori dinamici</string>
|
<string name="settings_dynamic_colors">Usa colori dinamici</string>
|
||||||
|
<string name="settings_open_url_external">Apri URL su browser esterno</string>
|
||||||
|
|
||||||
<string name="community_button_subscribe">Iscriviti</string>
|
<string name="community_button_subscribe">Iscriviti</string>
|
||||||
<string name="community_button_subscribed">Iscritto</string>
|
<string name="community_button_subscribed">Iscritto</string>
|
||||||
|
@ -77,7 +77,13 @@ fun App() {
|
|||||||
LaunchedEffect(lang) {}
|
LaunchedEffect(lang) {}
|
||||||
|
|
||||||
BottomSheetNavigator {
|
BottomSheetNavigator {
|
||||||
Navigator(MainScreen()) {
|
Navigator(
|
||||||
|
screen = MainScreen(),
|
||||||
|
onBackPressed = {
|
||||||
|
val callback = navigationCoordinator.getCanGoBackCallback()
|
||||||
|
callback?.let { it() } ?: true
|
||||||
|
}
|
||||||
|
) {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
navigationCoordinator.setRootNavigator(navigator)
|
navigationCoordinator.setRootNavigator(navigator)
|
||||||
CurrentScreen()
|
CurrentScreen()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user