diff --git a/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt b/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt new file mode 100644 index 000000000..09d09e2c9 --- /dev/null +++ b/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt @@ -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 + }, + ) +} diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt new file mode 100644 index 000000000..fe0d88879 --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt @@ -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, +) diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebViewNavigator.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebViewNavigator.kt new file mode 100644 index 000000000..a54b81ddc --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebViewNavigator.kt @@ -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() + + fun goBack() { + coroutineScope.launch { + events.emit(WebViewNavigationEvent.GoBack) + } + } +} + +@Composable +fun rememberWebViewNavigator(): WebViewNavigator { + val scope = rememberCoroutineScope() + return remember { + WebViewNavigator( + coroutineScope = scope + ) + } +} \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardBody.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardBody.kt index 88f7ab915..80b5fbe17 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardBody.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardBody.kt @@ -2,16 +2,26 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember 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.model.markdownColor 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 fun PostCardBody( modifier: Modifier = Modifier, text: String, ) { + val uriHandler = LocalUriHandler.current + val navigator = remember { getNavigationCoordinator().getRootNavigator() } + val keyStore = remember { getTemporaryKeyStore() } + if (text.isNotEmpty()) { Markdown( modifier = modifier, @@ -30,6 +40,14 @@ fun PostCardBody( text = MaterialTheme.colorScheme.onSurfaceVariant, backgroundCode = MaterialTheme.colorScheme.surfaceVariant, ), + onOpenUrl = { url -> + val openExternal = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false] + if (openExternal) { + uriHandler.openUri(url) + } else { + navigator?.push(WebViewScreen(url)) + } + } ) } } diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardTitle.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardTitle.kt index 90c733094..71fa9fbc9 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardTitle.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCardTitle.kt @@ -2,16 +2,26 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember 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.model.markdownColor 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 fun PostCardTitle( text: String, modifier: Modifier = Modifier, ) { + val uriHandler = LocalUriHandler.current + val navigator = remember { getNavigationCoordinator().getRootNavigator() } + val keyStore = remember { getTemporaryKeyStore() } + Markdown( modifier = modifier, content = text, @@ -22,5 +32,13 @@ fun PostCardTitle( text = MaterialTheme.colorScheme.onSurfaceVariant, backgroundCode = MaterialTheme.colorScheme.surfaceVariant, ), + onOpenUrl = { url -> + val openExternal = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false] + if (openExternal) { + uriHandler.openUri(url) + } else { + navigator?.push(WebViewScreen(url)) + } + } ) } diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostLinkBanner.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostLinkBanner.kt index 7d329bc4a..428021ee1 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostLinkBanner.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostLinkBanner.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.raccoonforlemmy.core.appearance.theme.CornerSize 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 fun PostLinkBanner( @@ -25,6 +30,9 @@ fun PostLinkBanner( url: String, ) { val uriHandler = LocalUriHandler.current + val navigator = remember { getNavigationCoordinator().getRootNavigator() } + val keyStore = remember { getTemporaryKeyStore() } + if (url.isNotEmpty()) { Row( modifier = modifier @@ -32,7 +40,12 @@ fun PostLinkBanner( color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f), shape = RoundedCornerShape(CornerSize.l), ).onClick { - uriHandler.openUri(url) + val openExternal = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false] + if (openExternal) { + uriHandler.openUri(url) + } else { + navigator?.push(WebViewScreen(url)) + } }.padding( horizontal = Spacing.m, vertical = Spacing.s, diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/DefaultNavigationCoordinator.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/DefaultNavigationCoordinator.kt index 8fa6a82f0..74afb21ff 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/DefaultNavigationCoordinator.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/DefaultNavigationCoordinator.kt @@ -16,6 +16,7 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator { private var navigator: Navigator? = null private var currentTab: Tab? = null private val scope = CoroutineScope(SupervisorJob()) + private var canGoBackCallback: (() -> Boolean)? = null override fun setRootNavigator(value: Navigator?) { navigator = value @@ -38,4 +39,10 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator { } } } + + override fun setCanGoBackCallback(value: (() -> Boolean)?) { + canGoBackCallback = value + } + + override fun getCanGoBackCallback(): (() -> Boolean)? = canGoBackCallback } \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/NavigationCoordinator.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/NavigationCoordinator.kt index d39588404..f654905d6 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/NavigationCoordinator.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/navigation/NavigationCoordinator.kt @@ -13,6 +13,10 @@ interface NavigationCoordinator { fun setRootNavigator(value: Navigator?) + fun setCanGoBackCallback(value: (() -> Boolean)?) + + fun getCanGoBackCallback(): (() -> Boolean)? + fun getRootNavigator(): Navigator? fun setBottomBarScrollConnection(value: NestedScrollConnection?) diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/web/WebViewScreen.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/web/WebViewScreen.kt new file mode 100644 index 000000000..4d199744f --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/web/WebViewScreen.kt @@ -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, + ) + } + } + } +} \ No newline at end of file diff --git a/core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt b/core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt new file mode 100644 index 000000000..3528f4bb9 --- /dev/null +++ b/core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CustomWebView.kt @@ -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 + } + ) +} diff --git a/core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/TestDropdown.kt b/core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/Dropdown.kt similarity index 100% rename from core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/TestDropdown.kt rename to core-commonui/src/iosMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/Dropdown.kt diff --git a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/Markdown.kt b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/Markdown.kt index 6893f9d6c..dca66c188 100755 --- a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/Markdown.kt +++ b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/Markdown.kt @@ -59,6 +59,7 @@ fun Markdown( padding: MarkdownPadding = markdownPadding(), modifier: Modifier = Modifier.fillMaxSize(), flavour: MarkdownFlavourDescriptor = GFMFlavourDescriptor(), + onOpenUrl: ((String) -> Unit)? = null, ) { CompositionLocalProvider( LocalReferenceLinkHandler provides ReferenceLinkHandlerImpl(), @@ -69,9 +70,9 @@ fun Markdown( Column(modifier) { val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content) parsedTree.children.forEach { node -> - if (!node.handleElement(content)) { + if (!node.handleElement(content, onOpenUrl)) { node.children.forEach { child -> - child.handleElement(content) + child.handleElement(content, onOpenUrl) } } } @@ -80,12 +81,15 @@ fun Markdown( } @Composable -private fun ASTNode.handleElement(content: String): Boolean { +private fun ASTNode.handleElement( + content: String, + onOpenUrl: ((String) -> Unit)? = null, +): Boolean { val typography = LocalMarkdownTypography.current var handled = true Spacer(Modifier.height(LocalMarkdownPadding.current.block)) when (type) { - TEXT -> MarkdownText(getTextInNode(content).toString()) + TEXT -> MarkdownText(getTextInNode(content).toString(), onOpenUrl = onOpenUrl) EOL -> {} CODE_FENCE -> MarkdownCodeFence(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_6 -> MarkdownHeader(content, this, typography.h6) 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) { - MarkdownOrderedList(content, this@handleElement, style = typography.ordered) + MarkdownOrderedList( + content, + this@handleElement, + style = typography.ordered, + onOpenUrl = onOpenUrl + ) } 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) diff --git a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt index 48695f810..a58bbf247 100755 --- a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt +++ b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownList.kt @@ -65,6 +65,7 @@ internal fun MarkdownOrderedList( node: ASTNode, style: TextStyle = LocalMarkdownTypography.current.ordered, level: Int = 0, + onOpenUrl: ((String) -> Unit)? = null, ) { val orderedListHandler = LocalOrderedListHandler.current MarkdownListItems(content, node, style, level) { child -> @@ -81,7 +82,12 @@ internal fun MarkdownOrderedList( buildMarkdownAnnotatedString(content, child.children.filterNonListTypes()) 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, style: TextStyle = LocalMarkdownTypography.current.bullet, level: Int = 0, + onOpenUrl: ((String) -> Unit)? = null, ) { val bulletHandler = LocalBulletListHandler.current MarkdownListItems(content, node, style, level) { child -> @@ -108,7 +115,12 @@ internal fun MarkdownBulletList( buildMarkdownAnnotatedString(content, child.children.filterNonListTypes()) pop() } - MarkdownText(text, Modifier.padding(bottom = 4.dp), style = style) + MarkdownText( + text, + Modifier.padding(bottom = 4.dp), + style = style, + onOpenUrl = onOpenUrl + ) } } } diff --git a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt index cf8d2f519..5d9d6f344 100755 --- a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt +++ b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownParagraph.kt @@ -12,11 +12,12 @@ internal fun MarkdownParagraph( content: String, node: ASTNode, style: TextStyle = LocalMarkdownTypography.current.paragraph, + onOpenUrl: ((String) -> Unit)? = null, ) { val styledText = buildAnnotatedString { pushStyle(style.toSpanStyle()) buildMarkdownAnnotatedString(content, node) pop() } - MarkdownText(styledText, style = style) + MarkdownText(styledText, style = style, onOpenUrl = onOpenUrl) } diff --git a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt index 1254d2711..cd32da355 100755 --- a/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt +++ b/core-md/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/markdown/compose/elements/MarkdownText.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -30,8 +29,9 @@ internal fun MarkdownText( content: String, modifier: Modifier = Modifier, style: TextStyle = LocalMarkdownTypography.current.text, + onOpenUrl: ((String) -> Unit)? = null, ) { - MarkdownText(AnnotatedString(content), modifier, style) + MarkdownText(AnnotatedString(content), modifier, style, onOpenUrl) } @Composable @@ -39,8 +39,8 @@ internal fun MarkdownText( content: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = LocalMarkdownTypography.current.text, + onOpenUrl: ((String) -> Unit)? = null, ) { - val uriHandler = LocalUriHandler.current val referenceLinkHandler = LocalReferenceLinkHandler.current val layoutResult = remember { mutableStateOf(null) } @@ -52,7 +52,10 @@ internal fun MarkdownText( val position = layoutResult.getOffsetForPosition(pos) content.getStringAnnotations(TAG_URL, position, position) .firstOrNull() - ?.let { uriHandler.openUri(referenceLinkHandler.find(it.item)) } + ?.let { + val url = referenceLinkHandler.find(it.item) + onOpenUrl?.invoke(url) + } } } } diff --git a/core-preferences/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/preferences/KeyStoreKeys.kt b/core-preferences/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/preferences/KeyStoreKeys.kt index 05198aa51..5eb51adea 100644 --- a/core-preferences/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/preferences/KeyStoreKeys.kt +++ b/core-preferences/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/preferences/KeyStoreKeys.kt @@ -13,4 +13,5 @@ object KeyStoreKeys { const val BlurNsfw = "blurNsfw" const val NavItemTitlesVisible = "navItemTitlesVisible" const val DynamicColors = "dynamicColors" + const val OpenUrlsInExternalBrowser = "openUrlsInExternalBrowser" } diff --git a/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreen.kt b/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreen.kt index 449345008..2444283a2 100644 --- a/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreen.kt +++ b/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreen.kt @@ -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 SettingsSwitchRow( title = stringResource(MR.strings.settings_include_nsfw), diff --git a/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenMviModel.kt b/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenMviModel.kt index 24faca524..0fef08232 100644 --- a/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenMviModel.kt +++ b/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenMviModel.kt @@ -20,6 +20,7 @@ interface SettingsScreenMviModel : data class ChangeDynamicColors(val value: Boolean) : Intent data class ChangeIncludeNsfw(val value: Boolean) : Intent data class ChangeBlurNsfw(val value: Boolean) : Intent + data class ChangeOpenUrlsInExternalBrowser(val value: Boolean) : Intent } data class UiState( @@ -35,6 +36,7 @@ interface SettingsScreenMviModel : val dynamicColors: Boolean = false, val includeNsfw: Boolean = true, val blurNsfw: Boolean = true, + val openUrlsInExternalBrowser: Boolean = false, val appVersion: String = "", ) diff --git a/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenViewModel.kt b/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenViewModel.kt index cb3ad1be5..82c40b11e 100644 --- a/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenViewModel.kt +++ b/feature-settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/content/SettingsScreenViewModel.kt @@ -82,6 +82,7 @@ class SettingsScreenViewModel( val listingType = keyStore[KeyStoreKeys.DefaultListingType, 0].toListingType() val postSortType = keyStore[KeyStoreKeys.DefaultPostSortType, 0].toSortType() val commentSortType = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType() + val openUrlsInExternalBrowser = keyStore[KeyStoreKeys.OpenUrlsInExternalBrowser, false] mvi.updateState { it.copy( defaultListingType = listingType, @@ -89,8 +90,9 @@ class SettingsScreenViewModel( defaultCommentSortType = commentSortType, includeNsfw = keyStore[KeyStoreKeys.IncludeNsfw, true], blurNsfw = keyStore[KeyStoreKeys.BlurNsfw, true], - appVersion = AppInfo.versionCode, supportsDynamicColors = colorSchemeProvider.supportsDynamicColors, + openUrlsInExternalBrowser = openUrlsInExternalBrowser, + appVersion = AppInfo.versionCode, ) } } @@ -136,6 +138,10 @@ class SettingsScreenViewModel( is SettingsScreenMviModel.Intent.ChangeDynamicColors -> { changeDynamicColors(intent.value) } + + is SettingsScreenMviModel.Intent.ChangeOpenUrlsInExternalBrowser -> { + changeOpenUrlsInExternalBrowser(intent.value) + } } } @@ -188,4 +194,9 @@ class SettingsScreenViewModel( themeRepository.changeDynamicColors(value) keyStore.save(KeyStoreKeys.DynamicColors, value) } + + private fun changeOpenUrlsInExternalBrowser(value: Boolean) { + mvi.updateState { it.copy(openUrlsInExternalBrowser = value) } + keyStore.save(KeyStoreKeys.OpenUrlsInExternalBrowser, value) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de87db5e8..93a0ba89f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ androidx_activity_compose = "1.7.2" androidx_crypto = "1.0.0" android_gradle = "7.4.2" -compose = "1.4.3" +compose = "1.5.1" crashlytics = "18.4.1" crashlytics_gradle = "2.9.9" gms_gradle = "4.3.15" diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 5bee9ade2..647f7b4c1 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -94,6 +94,7 @@ Blur NSFW images App version Use dynamic colors + Open URLs in external browser Subscribe Subscribed diff --git a/resources/src/commonMain/resources/MR/it/strings.xml b/resources/src/commonMain/resources/MR/it/strings.xml index 1913b89a3..ddb20bca2 100644 --- a/resources/src/commonMain/resources/MR/it/strings.xml +++ b/resources/src/commonMain/resources/MR/it/strings.xml @@ -92,6 +92,7 @@ Sfuma immagini NSFW Versione app Usa colori dinamici + Apri URL su browser esterno Iscriviti Iscritto diff --git a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt index b5a1dc3ec..1950439fe 100644 --- a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt +++ b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt @@ -77,7 +77,13 @@ fun App() { LaunchedEffect(lang) {} BottomSheetNavigator { - Navigator(MainScreen()) { + Navigator( + screen = MainScreen(), + onBackPressed = { + val callback = navigationCoordinator.getCanGoBackCallback() + callback?.let { it() } ?: true + } + ) { val navigator = LocalNavigator.current navigationCoordinator.setRootNavigator(navigator) CurrentScreen()