diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt index 3b071bc40..3801b2d52 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderEvent.kt @@ -23,4 +23,6 @@ sealed interface ReaderEvent { data object BackClicked : ReaderEvent data object TogglePostBookmark : ReaderEvent + + data object ArticleShortcutClicked : ReaderEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt index 397c44834..d922a345d 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt @@ -21,6 +21,7 @@ import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate import com.arkivanov.essenty.lifecycle.doOnCreate import dev.sasikanth.readability.Readability +import dev.sasikanth.rss.reader.core.network.post.PostSourceFetcher import dev.sasikanth.rss.reader.repository.RssRepository import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.util.relativeDurationString @@ -40,6 +41,7 @@ import me.tatarka.inject.annotations.Inject class ReaderPresenter( dispatchersProvider: DispatchersProvider, private val rssRepository: RssRepository, + private val postSourceFetcher: PostSourceFetcher, @Assisted private val postLink: String, @Assisted componentContext: ComponentContext, @Assisted private val goBack: () -> Unit @@ -50,7 +52,8 @@ class ReaderPresenter( PresenterInstance( dispatchersProvider = dispatchersProvider, rssRepository = rssRepository, - postLink = postLink + postLink = postLink, + postSourceFetcher = postSourceFetcher ) } @@ -75,6 +78,7 @@ class ReaderPresenter( private val dispatchersProvider: DispatchersProvider, private val rssRepository: RssRepository, private val postLink: String, + private val postSourceFetcher: PostSourceFetcher, ) : InstanceKeeper.Instance { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) @@ -94,6 +98,7 @@ class ReaderPresenter( /* no-op */ } ReaderEvent.TogglePostBookmark -> togglePostBookmark(postLink) + ReaderEvent.ArticleShortcutClicked -> articleShortcutClicked() } } @@ -139,7 +144,18 @@ class ReaderPresenter( } } - private suspend fun extractArticleHtmlContent(feedLink: String, content: String) = - withContext(dispatchersProvider.io) { Readability(feedLink, content) }.parse().content + private suspend fun extractArticleHtmlContent(postLink: String, content: String) = + withContext(dispatchersProvider.io) { Readability(postLink, content) }.parse().content + + private fun articleShortcutClicked() { + coroutineScope.launch { + val content = postSourceFetcher.fetch(postLink) + if (content != null) { + _state.update { it.copy(isFetchingFullArticle = true) } + val htmlContent = extractArticleHtmlContent(postLink, content) + _state.update { it.copy(content = htmlContent, isFetchingFullArticle = false) } + } + } + } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderState.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderState.kt index fa0f7114d..e4e257a7e 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderState.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderState.kt @@ -26,7 +26,8 @@ internal data class ReaderState( val content: String?, val publishedAt: String?, val isBookmarked: Boolean?, - val feed: Feed? = null + val feed: Feed?, + val isFetchingFullArticle: Boolean? ) { val hasContent: Boolean @@ -42,7 +43,8 @@ internal data class ReaderState( content = null, publishedAt = null, isBookmarked = null, - feed = null + feed = null, + isFetchingFullArticle = null ) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt index fdf01bc3e..195946ff3 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt @@ -18,12 +18,12 @@ package dev.sasikanth.rss.reader.reader.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Close @@ -55,6 +55,7 @@ import dev.sasikanth.material.color.utilities.utils.StringUtils import dev.sasikanth.rss.reader.platform.LocalLinkHandler import dev.sasikanth.rss.reader.reader.ReaderEvent import dev.sasikanth.rss.reader.reader.ReaderPresenter +import dev.sasikanth.rss.reader.resources.icons.ArticleShortcut import dev.sasikanth.rss.reader.resources.icons.Bookmark import dev.sasikanth.rss.reader.resources.icons.Bookmarked import dev.sasikanth.rss.reader.resources.icons.Share @@ -107,27 +108,49 @@ internal fun ReaderScreen(presenter: ReaderPresenter, modifier: Modifier = Modif modifier = Modifier.fillMaxWidth() .windowInsetsPadding(WindowInsets.navigationBars) - .padding(vertical = 8.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.weight(1f)) - val bookmarkIcon = - if (state.isBookmarked == true) { - TwineIcons.Bookmarked + Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { + val bookmarkIcon = + if (state.isBookmarked == true) { + TwineIcons.Bookmarked + } else { + TwineIcons.Bookmark + } + IconButton(onClick = { presenter.dispatch(ReaderEvent.TogglePostBookmark) }) { + Icon(bookmarkIcon, contentDescription = null) + } + } + + Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { + if (state.isFetchingFullArticle == true) { + CircularProgressIndicator( + color = AppTheme.colorScheme.tintedForeground, + modifier = Modifier.requiredSize(24.dp) + ) } else { - TwineIcons.Bookmark + IconButton( + onClick = { + coroutineScope.launch { presenter.dispatch(ReaderEvent.ArticleShortcutClicked) } + } + ) { + Icon(TwineIcons.ArticleShortcut, contentDescription = null) + } } - IconButton(onClick = { presenter.dispatch(ReaderEvent.TogglePostBookmark) }) { - Icon(bookmarkIcon, contentDescription = null) } - Spacer(Modifier.weight(1f)) - IconButton(onClick = { coroutineScope.launch { linkHandler.openLink(state.link) } }) { - Icon(TwineIcons.Website, contentDescription = null) + + Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { + IconButton(onClick = { coroutineScope.launch { linkHandler.openLink(state.link) } }) { + Icon(TwineIcons.Website, contentDescription = null) + } } - Spacer(Modifier.weight(1f)) - IconButton(onClick = { coroutineScope.launch { sharedHandler.share(state.link) } }) { - Icon(TwineIcons.Share, contentDescription = null) + + Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { + IconButton(onClick = { coroutineScope.launch { sharedHandler.share(state.link) } }) { + Icon(TwineIcons.Share, contentDescription = null) + } } - Spacer(Modifier.weight(1f)) } } }, @@ -163,11 +186,12 @@ internal fun ReaderScreen(presenter: ReaderPresenter, modifier: Modifier = Modif val dividerColor = StringUtils.hexFromArgb(AppTheme.colorScheme.surfaceContainerHigh.toArgb()) - val htmlTemplate = remember { - // TODO: Extract out the HTML rendering and customisation to separate class - // with actual templating - // language=HTML - """ + val htmlTemplate = + remember(state.content) { + // TODO: Extract out the HTML rendering and customisation to separate class + // with actual templating + // language=HTML + """
@@ -299,8 +323,8 @@ internal fun ReaderScreen(presenter: ReaderPresenter, modifier: Modifier = Modif """ - .trimIndent() - } + .trimIndent() + } val webViewState = rememberWebViewStateWithHTMLData(htmlTemplate) Box(Modifier.fillMaxSize().padding(paddingValues).padding(horizontal = 16.dp)) { @@ -308,7 +332,8 @@ internal fun ReaderScreen(presenter: ReaderPresenter, modifier: Modifier = Modif modifier = Modifier.fillMaxSize(), state = webViewState, navigator = navigator, - webViewJsBridge = jsBridge + webViewJsBridge = jsBridge, + captureBackPresses = false ) } }