From 6531d51d737e93a8babce0d1fd3d4165aa7ee435 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Fri, 26 Jan 2024 07:39:08 +0530 Subject: [PATCH 1/4] Add `PostSourceFetcher` --- .../core/network/fetcher/PostSourceFetcher.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt diff --git a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt new file mode 100644 index 000000000..ed0b07a93 --- /dev/null +++ b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.core.network.fetcher + +import dev.sasikanth.rss.reader.di.scopes.AppScope +import dev.sasikanth.rss.reader.util.DispatchersProvider +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.withContext +import me.tatarka.inject.annotations.Inject + +@Inject +@AppScope +class PostSourceFetcher( + private val httpClient: HttpClient, + private val dispatchersProvider: DispatchersProvider +) { + + suspend fun fetch(link: String): String? { + return withContext(dispatchersProvider.io) { + val response = httpClient.get(link) + if (response.status == HttpStatusCode.OK) { + response.bodyAsText() + } else { + null + } + } + } +} From d613ad03ad6d01524f1b2a0f1479e28c82364518 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Fri, 26 Jan 2024 08:21:34 +0530 Subject: [PATCH 2/4] Add `PostSourceFetcher` for fetching post HTML based on link --- .../reader/core/network/{fetcher => post}/PostSourceFetcher.kt | 2 +- .../kotlin/dev/sasikanth/rss/reader/di/ApplicationComponent.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) rename core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/{fetcher => post}/PostSourceFetcher.kt (96%) diff --git a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/post/PostSourceFetcher.kt similarity index 96% rename from core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt rename to core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/post/PostSourceFetcher.kt index ed0b07a93..2ed83d850 100644 --- a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/fetcher/PostSourceFetcher.kt +++ b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/post/PostSourceFetcher.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.sasikanth.rss.reader.core.network.fetcher +package dev.sasikanth.rss.reader.core.network.post import dev.sasikanth.rss.reader.di.scopes.AppScope import dev.sasikanth.rss.reader.util.DispatchersProvider diff --git a/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/di/ApplicationComponent.kt b/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/di/ApplicationComponent.kt index bac00b6ba..feef23eb6 100644 --- a/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/di/ApplicationComponent.kt +++ b/shared/src/androidMain/kotlin/dev/sasikanth/rss/reader/di/ApplicationComponent.kt @@ -18,6 +18,7 @@ package dev.sasikanth.rss.reader.di import android.content.Context import android.os.Build import dev.sasikanth.rss.reader.app.AppInfo +import dev.sasikanth.rss.reader.core.network.post.PostSourceFetcher import dev.sasikanth.rss.reader.di.scopes.AppScope import dev.sasikanth.rss.reader.repository.RssRepository import dev.sasikanth.rss.reader.repository.SettingsRepository @@ -33,6 +34,8 @@ abstract class ApplicationComponent(@get:Provides val context: Context) : abstract val settingsRepository: SettingsRepository + abstract val postSourceFetcher: PostSourceFetcher + @Provides @AppScope fun providesAppInfo(context: Context): AppInfo { From a247b16cb8eb73c1869a79eaf2f67c0a801722ac Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Fri, 26 Jan 2024 08:21:54 +0530 Subject: [PATCH 3/4] Add article shortcut iocn --- .../reader/resources/icons/ArticleShortcut.kt | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 resources/icons/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/icons/ArticleShortcut.kt diff --git a/resources/icons/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/icons/ArticleShortcut.kt b/resources/icons/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/icons/ArticleShortcut.kt new file mode 100644 index 000000000..d0ef7bb0a --- /dev/null +++ b/resources/icons/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/icons/ArticleShortcut.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.resources.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val TwineIcons.ArticleShortcut: ImageVector + get() { + if (articleShortcut != null) { + return articleShortcut!! + } + articleShortcut = + Builder( + name = "ArticleShortcut", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 960.0f, + viewportHeight = 960.0f + ) + .apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(400.0f, 680.0f) + horizontalLineToRelative(160.0f) + verticalLineToRelative(-80.0f) + lineTo(400.0f, 600.0f) + verticalLineToRelative(80.0f) + close() + moveTo(400.0f, 520.0f) + horizontalLineToRelative(280.0f) + verticalLineToRelative(-80.0f) + lineTo(400.0f, 440.0f) + verticalLineToRelative(80.0f) + close() + moveTo(280.0f, 360.0f) + horizontalLineToRelative(400.0f) + verticalLineToRelative(-80.0f) + lineTo(280.0f, 280.0f) + verticalLineToRelative(80.0f) + close() + moveTo(480.0f, 480.0f) + close() + moveTo(265.0f, 880.0f) + quadToRelative(-79.0f, 0.0f, -134.5f, -55.5f) + reflectiveQuadTo(75.0f, 690.0f) + quadToRelative(0.0f, -57.0f, 29.5f, -102.0f) + reflectiveQuadToRelative(77.5f, -68.0f) + lineTo(80.0f, 520.0f) + verticalLineToRelative(-80.0f) + horizontalLineToRelative(240.0f) + verticalLineToRelative(240.0f) + horizontalLineToRelative(-80.0f) + verticalLineToRelative(-97.0f) + quadToRelative(-37.0f, 8.0f, -61.0f, 38.0f) + reflectiveQuadToRelative(-24.0f, 69.0f) + quadToRelative(0.0f, 46.0f, 32.5f, 78.0f) + reflectiveQuadToRelative(77.5f, 32.0f) + verticalLineToRelative(80.0f) + close() + moveTo(400.0f, 840.0f) + verticalLineToRelative(-80.0f) + horizontalLineToRelative(360.0f) + verticalLineToRelative(-560.0f) + lineTo(200.0f, 200.0f) + verticalLineToRelative(160.0f) + horizontalLineToRelative(-80.0f) + verticalLineToRelative(-160.0f) + quadToRelative(0.0f, -33.0f, 23.5f, -56.5f) + reflectiveQuadTo(200.0f, 120.0f) + horizontalLineToRelative(560.0f) + quadToRelative(33.0f, 0.0f, 56.5f, 23.5f) + reflectiveQuadTo(840.0f, 200.0f) + verticalLineToRelative(560.0f) + quadToRelative(0.0f, 33.0f, -23.5f, 56.5f) + reflectiveQuadTo(760.0f, 840.0f) + lineTo(400.0f, 840.0f) + close() + } + } + .build() + return articleShortcut!! + } + +private var articleShortcut: ImageVector? = null From b8ada9ddb0a70002abc4aba29fc1ee0ac2769adf Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Fri, 26 Jan 2024 08:23:41 +0530 Subject: [PATCH 4/4] When article shortcut is clicked, then fetch the article source --- .../rss/reader/reader/ReaderEvent.kt | 2 + .../rss/reader/reader/ReaderPresenter.kt | 22 +++++- .../rss/reader/reader/ReaderState.kt | 6 +- .../rss/reader/reader/ui/ReaderScreen.kt | 73 +++++++++++++------ 4 files changed, 74 insertions(+), 29 deletions(-) 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 ) } }