From 2d20ac2d6b97d455f08996113775b8e402785e5d Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 28 Oct 2023 14:47:29 +0530 Subject: [PATCH] Add OPML section to settings screen --- .../resources/strings/EnTwineStrings.kt | 6 + .../reader/resources/strings/TwineStrings.kt | 6 + .../rss/reader/settings/SettingsEvent.kt | 6 + .../rss/reader/settings/SettingsPresenter.kt | 30 +++- .../rss/reader/settings/SettingsState.kt | 5 +- .../rss/reader/settings/ui/SettingsScreen.kt | 135 +++++++++++++++--- 6 files changed, 167 insertions(+), 21 deletions(-) diff --git a/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/EnTwineStrings.kt b/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/EnTwineStrings.kt index 6faaa3079..5823b4056 100644 --- a/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/EnTwineStrings.kt +++ b/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/EnTwineStrings.kt @@ -62,6 +62,7 @@ val EnTwineStrings = moreMenuOptions = "More menu options", settingsHeaderBehaviour = "Behavior", settingsHeaderFeedback = "Feedback & bug reports", + settingsHeaderOpml = "OPML", settingsBrowserTypeTitle = "Use in-app browser", settingsBrowserTypeSubtitle = "When turned off, links will open in your default browser.", settingsEnableBlurTitle = "Enable blur in homepage", @@ -71,6 +72,11 @@ val EnTwineStrings = settingsVersion = { versionName, versionCode -> "$versionName ($versionCode)" }, settingsAboutTitle = "About Twine", settingsAboutSubtitle = "Get to know the authors", + settingsOpmlImport = "Import", + settingsOpmlExport = "Export", + settingsOpmlImporting = { progress -> "Importing..$progress%" }, + settingsOpmlExporting = { progress -> "Exporting..$progress%" }, + settingsOpmlCancel = "Cancel", feeds = "Feeds", editFeeds = "Edit feeds", comments = "Comments", diff --git a/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/TwineStrings.kt b/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/TwineStrings.kt index 2a4ae6d1b..6af0cb943 100644 --- a/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/TwineStrings.kt +++ b/resources/strings/src/commonMain/kotlin/dev/sasikanth/rss/reader/resources/strings/TwineStrings.kt @@ -50,6 +50,7 @@ data class TwineStrings( val settings: String, val moreMenuOptions: String, val settingsHeaderBehaviour: String, + val settingsHeaderOpml: String, val settingsHeaderFeedback: String, val settingsBrowserTypeTitle: String, val settingsBrowserTypeSubtitle: String, @@ -59,6 +60,11 @@ data class TwineStrings( val settingsVersion: (String, Int) -> String, val settingsAboutTitle: String, val settingsAboutSubtitle: String, + val settingsOpmlImport: String, + val settingsOpmlExport: String, + val settingsOpmlImporting: (Int) -> String, + val settingsOpmlExporting: (Int) -> String, + val settingsOpmlCancel: String, val feeds: String, val editFeeds: String, val comments: String, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt index 0c1140686..76e69a78d 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt @@ -26,4 +26,10 @@ sealed interface SettingsEvent { data class ToggleFeaturedItemBlur(val value: Boolean) : SettingsEvent object AboutClicked : SettingsEvent + + object ImportOpmlClicked : SettingsEvent + + object ExportOpmlClicked : SettingsEvent + + object CancelOpmlImportOrExport : SettingsEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt index 1d3f23e85..8dec5c58b 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt @@ -19,6 +19,7 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate import dev.sasikanth.rss.reader.app.AppInfo +import dev.sasikanth.rss.reader.opml.OpmlManager import dev.sasikanth.rss.reader.repository.BrowserType import dev.sasikanth.rss.reader.repository.RssRepository import dev.sasikanth.rss.reader.repository.SettingsRepository @@ -44,6 +45,7 @@ class SettingsPresenter( private val settingsRepository: SettingsRepository, private val rssRepository: RssRepository, private val appInfo: AppInfo, + private val opmlManager: OpmlManager, @Assisted componentContext: ComponentContext, @Assisted private val goBack: () -> Unit, @Assisted private val openAbout: () -> Unit, @@ -55,7 +57,8 @@ class SettingsPresenter( dispatchersProvider = dispatchersProvider, appInfo = appInfo, settingsRepository = settingsRepository, - rssRepository = rssRepository + rssRepository = rssRepository, + opmlManager = opmlManager, ) } @@ -76,8 +79,9 @@ class SettingsPresenter( private class PresenterInstance( dispatchersProvider: DispatchersProvider, appInfo: AppInfo, - rssRepository: RssRepository, + private val rssRepository: RssRepository, private val settingsRepository: SettingsRepository, + private val opmlManager: OpmlManager, ) : InstanceKeeper.Instance { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) @@ -109,6 +113,13 @@ class SettingsPresenter( } } .launchIn(coroutineScope) + + opmlManager.result + .onEach { result -> + println(result) + _state.update { it.copy(opmlResult = result) } + } + .launchIn(coroutineScope) } fun dispatch(event: SettingsEvent) { @@ -121,9 +132,24 @@ class SettingsPresenter( SettingsEvent.AboutClicked -> { // no-op } + SettingsEvent.ImportOpmlClicked -> importOpmlClicked() + SettingsEvent.ExportOpmlClicked -> exportOpmlClicked() + SettingsEvent.CancelOpmlImportOrExport -> cancelOpmlImportOrExport() } } + private fun cancelOpmlImportOrExport() { + opmlManager.cancel() + } + + private fun exportOpmlClicked() { + coroutineScope.launch { opmlManager.export() } + } + + private fun importOpmlClicked() { + coroutineScope.launch { opmlManager.import() } + } + private fun toggleFeaturedItemBlur(value: Boolean) { coroutineScope.launch { settingsRepository.toggleFeaturedItemBlur(value) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt index bf9ae8865..1c0faab64 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt @@ -17,6 +17,7 @@ package dev.sasikanth.rss.reader.settings import androidx.compose.runtime.Immutable import dev.sasikanth.rss.reader.app.AppInfo +import dev.sasikanth.rss.reader.opml.OpmlResult import dev.sasikanth.rss.reader.repository.BrowserType @Immutable @@ -25,6 +26,7 @@ internal data class SettingsState( val enableHomePageBlur: Boolean, val hasFeeds: Boolean, val appInfo: AppInfo, + val opmlResult: OpmlResult?, ) { companion object { @@ -34,7 +36,8 @@ internal data class SettingsState( browserType = BrowserType.Default, enableHomePageBlur = true, hasFeeds = false, - appInfo = appInfo + appInfo = appInfo, + opmlResult = null ) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt index 63eb7a291..10d40d006 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt @@ -16,6 +16,7 @@ package dev.sasikanth.rss.reader.settings.ui import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -26,12 +27,14 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.Divider as MaterialDivider import androidx.compose.material3.Icon @@ -56,10 +59,13 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import dev.sasikanth.rss.reader.app.AppInfo +import dev.sasikanth.rss.reader.components.OutlinedButton import dev.sasikanth.rss.reader.components.SubHeader import dev.sasikanth.rss.reader.components.image.AsyncImage +import dev.sasikanth.rss.reader.opml.OpmlResult import dev.sasikanth.rss.reader.platform.LocalLinkHandler import dev.sasikanth.rss.reader.repository.BrowserType import dev.sasikanth.rss.reader.resources.strings.LocalStrings @@ -133,7 +139,7 @@ internal fun SettingsScreen( } if (canBlurImage) { - item { InsetDivider() } + item { Divider(horizontalInsets = 24.dp) } item { FeaturedItemBlurSettingItem( @@ -145,13 +151,22 @@ internal fun SettingsScreen( } } + item { Divider(24.dp) } + item { - MaterialDivider( - modifier = Modifier.fillMaxWidth(), - color = AppTheme.colorScheme.surfaceContainer + OPMLSettingItem( + opmlResult = state.opmlResult, + hasFeeds = state.hasFeeds, + onImportClicked = { settingsPresenter.dispatch(SettingsEvent.ImportOpmlClicked) }, + onExportClicked = { settingsPresenter.dispatch(SettingsEvent.ExportOpmlClicked) }, + onCancelClicked = { + settingsPresenter.dispatch(SettingsEvent.CancelOpmlImportOrExport) + } ) } + item { Divider() } + item { SubHeader( text = LocalStrings.current.settingsHeaderFeedback, @@ -167,21 +182,11 @@ internal fun SettingsScreen( ) } - item { - MaterialDivider( - modifier = Modifier.fillMaxWidth(), - color = AppTheme.colorScheme.surfaceContainer - ) - } + item { Divider() } item { AboutItem { settingsPresenter.dispatch(SettingsEvent.AboutClicked) } } - item { - MaterialDivider( - modifier = Modifier.fillMaxWidth(), - color = AppTheme.colorScheme.surfaceContainer - ) - } + item { Divider() } } } }, @@ -307,6 +312,100 @@ private fun BrowserTypeSettingItem( } } +@Composable +private fun OPMLSettingItem( + opmlResult: OpmlResult?, + hasFeeds: Boolean, + onImportClicked: () -> Unit, + onExportClicked: () -> Unit, + onCancelClicked: () -> Unit +) { + Column { + SubHeader(text = LocalStrings.current.settingsHeaderOpml) + + when (opmlResult) { + is OpmlResult.InProgress.Importing, + is OpmlResult.InProgress.Exporting -> { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = { + // no-op + }, + enabled = false, + colors = + ButtonDefaults.outlinedButtonColors( + containerColor = AppTheme.colorScheme.tintedSurface, + disabledContainerColor = AppTheme.colorScheme.tintedSurface, + contentColor = AppTheme.colorScheme.tintedForeground, + disabledContentColor = AppTheme.colorScheme.tintedForeground, + ), + border = null + ) { + val string = + when (opmlResult) { + is OpmlResult.InProgress.Importing -> { + LocalStrings.current.settingsOpmlImporting(opmlResult.progress) + } + is OpmlResult.InProgress.Exporting -> { + LocalStrings.current.settingsOpmlExporting(opmlResult.progress) + } + else -> { + "" + } + } + + Text(string) + } + + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = onCancelClicked, + colors = + ButtonDefaults.outlinedButtonColors( + containerColor = Color.Unspecified, + contentColor = AppTheme.colorScheme.tintedForeground, + ), + ) { + Text(LocalStrings.current.settingsOpmlCancel) + } + } + } + + // TODO: Handle error states + OpmlResult.Idle, + OpmlResult.Error.NoContentInOpmlFile, + is OpmlResult.Error.UnknownFailure, -> { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = onImportClicked, + ) { + Text(LocalStrings.current.settingsOpmlImport) + } + + OutlinedButton( + modifier = Modifier.weight(1f), + enabled = hasFeeds, + onClick = onExportClicked, + ) { + Text(LocalStrings.current.settingsOpmlExport) + } + } + } + null -> { + Box(Modifier.requiredHeight(64.dp)) + } + } + } +} + @Composable private fun ReportIssueItem(appInfo: AppInfo, onClick: () -> Unit) { Box(modifier = Modifier.clickable(onClick = onClick)) { @@ -398,9 +497,9 @@ private fun AboutProfileImages() { } @Composable -private fun InsetDivider() { +private fun Divider(horizontalInsets: Dp = 0.dp) { MaterialDivider( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 24.dp), + modifier = Modifier.padding(vertical = 8.dp, horizontal = horizontalInsets), color = AppTheme.colorScheme.surfaceContainer ) }