From e89351b800193b1293d4fa9465f8ebce9074a6e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 19:52:57 +0000 Subject: [PATCH 1/2] Update dependency gradle to v8.6 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e0930..a80b22ce5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 39ca8943c12b1bb01280ec208b9af49ec9a6d9a5 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 3 Feb 2024 09:27:12 +0530 Subject: [PATCH 2/2] Add option to show feed info sheet from feeds sheet (#276) * When feed sheet is created, then load feed info * When back is clicked, then dismiss bottom sheet * When remove feed is clicked, then remove feed * When feed name is changed, then update the feed name * Extract out feed label input component from `FeedListItem` * Add text align option to `FeedLabelInput` * Add `FeedInfoBottomSheet` UI * When feed info is clicked, then open feed info bottom sheet * Add support opening modals in the app * Use `ModalBottomSheet` as parent in `FeedInfoBottomSheet` * Dispatch `Init` when feed presenter is created * Remove `wrapContentHeight` modifier from `FeedInfoBottomSheet` * Run code formatting * Add progress indicator to `FeedInfoBottomSheet` * Add param to open feed info sheet, when button is clicked in feed list item * When removing feed is finished, then dismiss the feed info sheet * Change arrow down icon to info icon in feed list item * Show confirm delete feed dialog when remove button is clicked in feed info sheet --- .../resources/strings/EnTwineStrings.kt | 5 +- .../reader/resources/strings/TwineStrings.kt | 5 +- .../dev/sasikanth/rss/reader/app/App.kt | 73 +++++---- .../sasikanth/rss/reader/app/AppPresenter.kt | 40 ++++- .../dev/sasikanth/rss/reader/app/Modals.kt | 23 +++ .../components/ConfirmFeedDeleteDialog.kt | 75 +++++++++ .../rss/reader/components/FeedLabelInput.kt | 126 +++++++++++++++ .../sasikanth/rss/reader/feed/FeedEffect.kt | 22 +++ .../sasikanth/rss/reader/feed/FeedEvent.kt | 30 ++++ .../rss/reader/feed/FeedPresenter.kt | 123 ++++++++++++++ .../sasikanth/rss/reader/feed/FeedState.kt | 28 ++++ .../rss/reader/feed/ui/FeedInfoBottomSheet.kt | 151 ++++++++++++++++++ .../sasikanth/rss/reader/feeds/FeedsEvent.kt | 2 + .../rss/reader/feeds/FeedsPresenter.kt | 19 ++- .../rss/reader/feeds/ui/FeedListItem.kt | 106 +----------- .../rss/reader/feeds/ui/FeedsBottomSheet.kt | 6 +- .../rss/reader/home/HomePresenter.kt | 9 +- 17 files changed, 707 insertions(+), 136 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/Modals.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/ConfirmFeedDeleteDialog.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/FeedLabelInput.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEffect.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedState.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt 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 ef038eef6..d0a41f28f 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 @@ -113,5 +113,8 @@ val EnTwineStrings = openSourceDesc = "Twine is built on open source technologies and is completely free to use, you can find the source code of Twine and some of my other popular projects on GitHub. Click here to head over there.", markAsRead = "Mark as Read", - markAsUnRead = "Mark as Unread" + markAsUnRead = "Mark as Unread", + removeFeed = "Remove feed", + delete = "Delete", + removeFeedDesc = { "Do you want to remove \"${it}\"?" } ) 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 6df63b744..d024155d8 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 @@ -102,7 +102,10 @@ data class TwineStrings( val openSource: String, val openSourceDesc: String, val markAsRead: String, - val markAsUnRead: String + val markAsUnRead: String, + val removeFeed: String, + val delete: String, + val removeFeedDesc: (String) -> String ) object Locales { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt index 676d4ab0a..a31c3508e 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt @@ -15,23 +15,27 @@ */ package dev.sasikanth.rss.reader.app +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import coil3.ImageLoader import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation +import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import com.arkivanov.essenty.backhandler.BackHandler import dev.sasikanth.rss.reader.about.ui.AboutScreen import dev.sasikanth.rss.reader.bookmarks.ui.BookmarksScreen import dev.sasikanth.rss.reader.components.DynamicContentTheme import dev.sasikanth.rss.reader.components.LocalDynamicColorState import dev.sasikanth.rss.reader.components.rememberDynamicColorState +import dev.sasikanth.rss.reader.feed.ui.FeedInfoBottomSheet import dev.sasikanth.rss.reader.home.ui.HomeScreen import dev.sasikanth.rss.reader.platform.LinkHandler import dev.sasikanth.rss.reader.platform.LocalLinkHandler @@ -67,34 +71,49 @@ fun App( ) { DynamicContentTheme(dynamicColorState) { ProvideStrings { - Children( - modifier = Modifier.fillMaxSize(), - stack = appPresenter.screenStack, - animation = - backAnimation( - backHandler = appPresenter.backHandler, - onBack = appPresenter::onBackClicked - ) - ) { child -> - val fillMaxSizeModifier = Modifier.fillMaxSize() - when (val screen = child.instance) { - is Screen.Home -> { - HomeScreen(homePresenter = screen.presenter, modifier = fillMaxSizeModifier) + Box { + Children( + modifier = Modifier.fillMaxSize(), + stack = appPresenter.screenStack, + animation = + backAnimation( + backHandler = appPresenter.backHandler, + onBack = appPresenter::onBackClicked + ) + ) { child -> + val fillMaxSizeModifier = Modifier.fillMaxSize() + when (val screen = child.instance) { + is Screen.Home -> { + HomeScreen(homePresenter = screen.presenter, modifier = fillMaxSizeModifier) + } + is Screen.Search -> { + SearchScreen(searchPresenter = screen.presenter, modifier = fillMaxSizeModifier) + } + is Screen.Bookmarks -> { + BookmarksScreen( + bookmarksPresenter = screen.presenter, + modifier = fillMaxSizeModifier + ) + } + is Screen.Settings -> { + SettingsScreen(settingsPresenter = screen.presenter, modifier = fillMaxSizeModifier) + } + is Screen.About -> { + AboutScreen(aboutPresenter = screen.presenter, modifier = fillMaxSizeModifier) + } + is Screen.Reader -> { + ReaderScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) + } } - is Screen.Search -> { - SearchScreen(searchPresenter = screen.presenter, modifier = fillMaxSizeModifier) - } - is Screen.Bookmarks -> { - BookmarksScreen(bookmarksPresenter = screen.presenter, modifier = fillMaxSizeModifier) - } - is Screen.Settings -> { - SettingsScreen(settingsPresenter = screen.presenter, modifier = fillMaxSizeModifier) - } - is Screen.About -> { - AboutScreen(aboutPresenter = screen.presenter, modifier = fillMaxSizeModifier) - } - is Screen.Reader -> { - ReaderScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) + } + + val modals by appPresenter.modalStack.subscribeAsState() + modals.child?.instance?.also { modal -> + when (modal) { + is Modals.FeedInfo -> + FeedInfoBottomSheet( + feedPresenter = modal.presenter, + ) } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt index 69d32efb5..629080fab 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt @@ -16,6 +16,11 @@ package dev.sasikanth.rss.reader.app import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.slot.ChildSlot +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack @@ -30,6 +35,7 @@ import com.arkivanov.essenty.parcelable.Parcelize import dev.sasikanth.rss.reader.about.AboutPresenter import dev.sasikanth.rss.reader.bookmarks.BookmarksPresenter import dev.sasikanth.rss.reader.di.scopes.ActivityScope +import dev.sasikanth.rss.reader.feed.FeedPresenter import dev.sasikanth.rss.reader.home.HomePresenter import dev.sasikanth.rss.reader.reader.ReaderPresenter import dev.sasikanth.rss.reader.refresh.LastUpdatedAt @@ -50,6 +56,7 @@ private typealias HomePresenterFactory = openBookmarks: () -> Unit, openSettings: () -> Unit, openPost: (String) -> Unit, + openFeedInfo: (String) -> Unit, ) -> HomePresenter private typealias SearchPresentFactory = @@ -86,6 +93,13 @@ private typealias ReaderPresenterFactory = goBack: () -> Unit, ) -> ReaderPresenter +private typealias FeedPresenterFactory = + ( + feedLink: String, + ComponentContext, + dismiss: () -> Unit, + ) -> FeedPresenter + @Inject @ActivityScope class AppPresenter( @@ -97,6 +111,7 @@ class AppPresenter( private val settingsPresenter: SettingsPresenterFactory, private val aboutPresenter: AboutPresenterFactory, private val readerPresenter: ReaderPresenterFactory, + private val feedPresenter: FeedPresenterFactory, private val lastUpdatedAt: LastUpdatedAt, private val rssRepository: RssRepository ) : ComponentContext by componentContext { @@ -111,6 +126,7 @@ class AppPresenter( } private val navigation = StackNavigation() + private val modalNavigation = SlotNavigation() internal val screenStack: Value> = childStack( @@ -120,6 +136,13 @@ class AppPresenter( childFactory = ::createScreen, ) + internal val modalStack: Value> = + childSlot( + source = modalNavigation, + handleBackButton = true, + childFactory = ::createModal, + ) + init { lifecycle.doOnStart { presenterInstance.refreshFeedsIfExpired() } } @@ -128,6 +151,16 @@ class AppPresenter( navigation.pop() } + private fun createModal(modalConfig: ModalConfig, componentContext: ComponentContext): Modals = + when (modalConfig) { + is ModalConfig.FeedInfo -> { + Modals.FeedInfo( + presenter = + feedPresenter(modalConfig.feedLink, componentContext) { modalNavigation.dismiss() } + ) + } + } + private fun createScreen(config: Config, componentContext: ComponentContext): Screen = when (config) { Config.Home -> { @@ -138,7 +171,8 @@ class AppPresenter( { navigation.push(Config.Search) }, { navigation.push(Config.Bookmarks) }, { navigation.push(Config.Settings) }, - { navigation.push(Config.Reader(it)) } + { navigation.push(Config.Reader(it)) }, + { modalNavigation.activate(ModalConfig.FeedInfo(it)) } ) ) } @@ -217,4 +251,8 @@ class AppPresenter( @Parcelize data class Reader(val postLink: String) : Config } + + sealed interface ModalConfig : Parcelable { + @Parcelize data class FeedInfo(val feedLink: String) : ModalConfig + } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/Modals.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/Modals.kt new file mode 100644 index 000000000..dcbcc5459 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/Modals.kt @@ -0,0 +1,23 @@ +/* + * 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.app + +import dev.sasikanth.rss.reader.feed.FeedPresenter + +internal sealed interface Modals { + class FeedInfo(val presenter: FeedPresenter) : Modals +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/ConfirmFeedDeleteDialog.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/ConfirmFeedDeleteDialog.kt new file mode 100644 index 000000000..fd002dac6 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/ConfirmFeedDeleteDialog.kt @@ -0,0 +1,75 @@ +/* + * 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.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.sasikanth.rss.reader.resources.strings.LocalStrings +import dev.sasikanth.rss.reader.ui.AppTheme + +@Composable +internal fun ConfirmFeedDeleteDialog( + feedName: String, + onRemoveFeed: () -> Unit, + dismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + AlertDialog( + modifier = modifier, + onDismissRequest = dismiss, + confirmButton = { + TextButton( + onClick = { + onRemoveFeed() + dismiss() + }, + shape = MaterialTheme.shapes.large + ) { + Text( + text = LocalStrings.current.delete, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = dismiss, shape = MaterialTheme.shapes.large) { + Text( + text = LocalStrings.current.buttonCancel, + style = MaterialTheme.typography.labelLarge, + color = AppTheme.colorScheme.textEmphasisMed + ) + } + }, + title = { + Text(text = LocalStrings.current.removeFeed, color = AppTheme.colorScheme.textEmphasisMed) + }, + text = { + Text( + text = LocalStrings.current.removeFeedDesc(feedName), + color = AppTheme.colorScheme.textEmphasisMed + ) + }, + containerColor = AppTheme.colorScheme.tintedSurface, + titleContentColor = AppTheme.colorScheme.onSurface, + textContentColor = AppTheme.colorScheme.onSurface, + ) +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/FeedLabelInput.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/FeedLabelInput.kt new file mode 100644 index 000000000..b790a8580 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/FeedLabelInput.kt @@ -0,0 +1,126 @@ +/* + * 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.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.sasikanth.rss.reader.resources.strings.LocalStrings +import dev.sasikanth.rss.reader.ui.AppTheme +import kotlinx.coroutines.delay + +@Composable +internal fun FeedLabelInput( + value: String, + onFeedNameChanged: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + textAlign: TextAlign = TextAlign.Start +) { + // Maintaining local state so that it updates the text field in the UI + // instantly and doesn't have any weird UI state issues. + // + // I probably can extract this out into the presenter, we would have to + // maintain a list of text field states that are derived from the feeds list + // but this seems like a good alternative. + // + var input by remember(value) { mutableStateOf(value) } + var inputModified by remember(value) { mutableStateOf(false) } + + val focusManager = LocalFocusManager.current + val isInputBlank by derivedStateOf { input.isBlank() } + val interactionSource = remember { MutableInteractionSource() } + val isFocused by interactionSource.collectIsFocusedAsState() + + fun onFeedNameChanged(clearFocus: Boolean = true) { + inputModified = input != value + + if (!isInputBlank && inputModified) { + onFeedNameChanged.invoke(input) + } + + if (clearFocus) { + focusManager.clearFocus() + } + } + + LaunchedEffect(isFocused) { + if (!isFocused && !inputModified) { + input = value + } + } + + LaunchedEffect(input) { + // Same as setting a debounce + delay(500) + onFeedNameChanged(clearFocus = false) + } + + TextField( + modifier = modifier.requiredHeight(56.dp).fillMaxWidth(), + value = input, + onValueChange = { input = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, autoCorrect = false), + keyboardActions = KeyboardActions(onDone = { onFeedNameChanged() }), + singleLine = true, + textStyle = MaterialTheme.typography.titleMedium.copy(textAlign = textAlign), + shape = RoundedCornerShape(16.dp), + enabled = enabled, + interactionSource = interactionSource, + colors = + TextFieldDefaults.colors( + focusedContainerColor = AppTheme.colorScheme.tintedSurface, + unfocusedContainerColor = AppTheme.colorScheme.tintedSurface, + disabledContainerColor = AppTheme.colorScheme.tintedBackground, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + disabledTextColor = AppTheme.colorScheme.textEmphasisHigh, + focusedTextColor = AppTheme.colorScheme.textEmphasisHigh, + unfocusedTextColor = AppTheme.colorScheme.textEmphasisHigh, + ), + placeholder = { + Text( + text = LocalStrings.current.feedNameHint, + style = MaterialTheme.typography.labelLarge, + color = AppTheme.colorScheme.tintedForeground.copy(alpha = 0.4f) + ) + } + ) +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEffect.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEffect.kt new file mode 100644 index 000000000..d891b5ab0 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEffect.kt @@ -0,0 +1,22 @@ +/* + * 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.feed + +sealed interface FeedEffect { + + data object DismissSheet : FeedEffect +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt new file mode 100644 index 000000000..1cff60554 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedEvent.kt @@ -0,0 +1,30 @@ +/* + * 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.feed + +sealed interface FeedEvent { + + data object Init : FeedEvent + + data object BackClicked : FeedEvent + + data object RemoveFeedClicked : FeedEvent + + data class OnFeedNameChanged(val newFeedName: String, val feedLink: String) : FeedEvent + + data object DismissSheet : FeedEvent +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt new file mode 100644 index 000000000..805c1a653 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedPresenter.kt @@ -0,0 +1,123 @@ +/* + * 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.feed + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.arkivanov.essenty.lifecycle.doOnCreate +import dev.sasikanth.rss.reader.repository.RssRepository +import dev.sasikanth.rss.reader.util.DispatchersProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import me.tatarka.inject.annotations.Assisted +import me.tatarka.inject.annotations.Inject + +@Inject +class FeedPresenter( + dispatchersProvider: DispatchersProvider, + rssRepository: RssRepository, + @Assisted feedLink: String, + @Assisted componentContext: ComponentContext, + @Assisted private val dismiss: () -> Unit +) : ComponentContext by componentContext { + + private val presenterInstance = + instanceKeeper.getOrCreate { + PresenterInstance( + dispatchersProvider = dispatchersProvider, + rssRepository = rssRepository, + feedLink = feedLink + ) + } + + internal val state: StateFlow = presenterInstance.state + internal val effects = presenterInstance.effects.asSharedFlow() + + init { + lifecycle.doOnCreate { dispatch(FeedEvent.Init) } + } + + fun dispatch(event: FeedEvent) { + when (event) { + FeedEvent.BackClicked, + FeedEvent.DismissSheet -> dismiss() + else -> { + // no-op + } + } + + presenterInstance.dispatch(event) + } + + private class PresenterInstance( + dispatchersProvider: DispatchersProvider, + private val rssRepository: RssRepository, + private val feedLink: String + ) : InstanceKeeper.Instance { + + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + + private val _state = MutableStateFlow(FeedState.DEFAULT) + val state: StateFlow = + _state.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = FeedState.DEFAULT + ) + + val effects = MutableSharedFlow() + + fun dispatch(event: FeedEvent) { + when (event) { + FeedEvent.Init -> init() + FeedEvent.BackClicked, + FeedEvent.DismissSheet -> { + // no-op + } + FeedEvent.RemoveFeedClicked -> removeFeed() + is FeedEvent.OnFeedNameChanged -> onFeedNameUpdated(event.newFeedName, event.feedLink) + } + } + + private fun onFeedNameUpdated(newFeedName: String, feedLink: String) { + coroutineScope.launch { rssRepository.updateFeedName(newFeedName, feedLink) } + } + + private fun removeFeed() { + coroutineScope.launch { + rssRepository.removeFeed(feedLink) + effects.emit(FeedEffect.DismissSheet) + } + } + + private fun init() { + coroutineScope.launch { + val feed = rssRepository.feed(feedLink) + _state.update { it.copy(feed = feed) } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedState.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedState.kt new file mode 100644 index 000000000..e349f0218 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/FeedState.kt @@ -0,0 +1,28 @@ +/* + * 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.feed + +import androidx.compose.runtime.Immutable +import dev.sasikanth.rss.reader.core.model.local.Feed + +@Immutable +internal data class FeedState(val feed: Feed?) { + + companion object { + internal val DEFAULT = FeedState(feed = null) + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt new file mode 100644 index 000000000..28fba09d7 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt @@ -0,0 +1,151 @@ +/* + * 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.feed.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.sasikanth.rss.reader.components.ConfirmFeedDeleteDialog +import dev.sasikanth.rss.reader.components.FeedLabelInput +import dev.sasikanth.rss.reader.components.image.AsyncImage +import dev.sasikanth.rss.reader.core.model.local.Feed +import dev.sasikanth.rss.reader.feed.FeedEffect +import dev.sasikanth.rss.reader.feed.FeedEvent +import dev.sasikanth.rss.reader.feed.FeedPresenter +import dev.sasikanth.rss.reader.resources.strings.LocalStrings +import dev.sasikanth.rss.reader.ui.AppTheme +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun FeedInfoBottomSheet( + feedPresenter: FeedPresenter, + modifier: Modifier = Modifier, +) { + val state by feedPresenter.state.collectAsState() + + LaunchedEffect(Unit) { + feedPresenter.effects.collectLatest { effect -> + when (effect) { + FeedEffect.DismissSheet -> feedPresenter.dispatch(FeedEvent.DismissSheet) + } + } + } + + ModalBottomSheet( + modifier = Modifier.then(modifier), + onDismissRequest = { feedPresenter.dispatch(FeedEvent.BackClicked) }, + containerColor = AppTheme.colorScheme.tintedBackground, + contentColor = AppTheme.colorScheme.tintedForeground, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).padding(bottom = 16.dp) + ) { + Spacer(Modifier.requiredHeight(8.dp)) + + val feed = state.feed + if (feed != null) { + Box(Modifier.requiredSize(64.dp).background(Color.White, RoundedCornerShape(16.dp))) { + AsyncImage( + url = feed.icon, + contentDescription = feed.name, + modifier = + Modifier.requiredSize(56.dp).clip(RoundedCornerShape(12.dp)).align(Alignment.Center) + ) + } + + Spacer(Modifier.requiredHeight(24.dp)) + + FeedLabelInput( + value = feed.name, + onFeedNameChanged = { newFeedName -> + feedPresenter.dispatch(FeedEvent.OnFeedNameChanged(newFeedName, feed.link)) + }, + textAlign = TextAlign.Center + ) + + Spacer(Modifier.requiredHeight(8.dp)) + + RemoveFeedButton(feed) { feedPresenter.dispatch(FeedEvent.RemoveFeedClicked) } + } else { + CircularProgressIndicator(color = AppTheme.colorScheme.tintedForeground) + } + } + } +} + +@Composable +private fun RemoveFeedButton(feed: Feed, onRemoveFeedClick: () -> Unit) { + Box { + var showConfirmDialog by remember { mutableStateOf(false) } + + TextButton( + onClick = { showConfirmDialog = true }, + contentPadding = PaddingValues(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 24.dp), + shape = MaterialTheme.shapes.large + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = LocalStrings.current.editFeeds, + tint = MaterialTheme.colorScheme.error + ) + Spacer(Modifier.width(12.dp)) + Text( + text = LocalStrings.current.removeFeed, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.error + ) + } + + if (showConfirmDialog) { + ConfirmFeedDeleteDialog( + feedName = feed.name, + onRemoveFeed = onRemoveFeedClick, + dismiss = { showConfirmDialog = false }, + ) + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt index 0d49538d7..b46baedf5 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsEvent.kt @@ -37,4 +37,6 @@ sealed interface FeedsEvent { data object ClearSearchQuery : FeedsEvent data class MarkPostsInFeedAsReadClicked(val feedLink: String) : FeedsEvent + + data class OnFeedInfoClick(val feedLink: String) : FeedsEvent } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt index 9be7b6f46..b2640a3d6 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt @@ -71,7 +71,8 @@ class FeedsPresenter( private val rssRepository: RssRepository, private val settingsRepository: SettingsRepository, private val observableSelectedFeed: ObservableSelectedFeed, - @Assisted componentContext: ComponentContext + @Assisted componentContext: ComponentContext, + @Assisted private val openFeedInfo: (String) -> Unit, ) : ComponentContext by componentContext { private val presenterInstance = @@ -93,7 +94,18 @@ class FeedsPresenter( lifecycle.doOnCreate { presenterInstance.dispatch(FeedsEvent.Init) } } - fun dispatch(event: FeedsEvent) = presenterInstance.dispatch(event) + fun dispatch(event: FeedsEvent) { + when (event) { + is FeedsEvent.OnFeedInfoClick -> { + openFeedInfo(event.feedLink) + } + else -> { + // no-op + } + } + + presenterInstance.dispatch(event) + } private class PresenterInstance( dispatchersProvider: DispatchersProvider, @@ -128,6 +140,9 @@ class FeedsPresenter( FeedsEvent.ClearSearchQuery -> clearSearchQuery() is FeedsEvent.SearchQueryChanged -> onSearchQueryChanged(event.searchQuery) is FeedsEvent.MarkPostsInFeedAsReadClicked -> markPostsInFeedAsReadClicked(event.feedLink) + is FeedsEvent.OnFeedInfoClick -> { + // no-op + } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedListItem.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedListItem.kt index b95df8045..9e7bb7487 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedListItem.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedListItem.kt @@ -18,34 +18,25 @@ package dev.sasikanth.rss.reader.feeds.ui import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.requiredWidth import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.twotone.Info import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -57,17 +48,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import dev.sasikanth.rss.reader.components.DropdownMenu import dev.sasikanth.rss.reader.components.DropdownMenuItem +import dev.sasikanth.rss.reader.components.FeedLabelInput import dev.sasikanth.rss.reader.components.image.AsyncImage import dev.sasikanth.rss.reader.core.model.local.Feed import dev.sasikanth.rss.reader.feeds.ui.FeedsSheetMode.Edit import dev.sasikanth.rss.reader.platform.LocalLinkHandler -import dev.sasikanth.rss.reader.resources.icons.Delete import dev.sasikanth.rss.reader.resources.icons.DoneAll import dev.sasikanth.rss.reader.resources.icons.Pin import dev.sasikanth.rss.reader.resources.icons.PinFilled @@ -77,7 +66,6 @@ import dev.sasikanth.rss.reader.resources.icons.Website import dev.sasikanth.rss.reader.resources.strings.LocalStrings import dev.sasikanth.rss.reader.share.LocalShareHandler import dev.sasikanth.rss.reader.ui.AppTheme -import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable @@ -88,7 +76,7 @@ internal fun FeedListItem( canPinFeeds: Boolean, canShowUnreadPostsCount: Boolean, feedsSheetMode: FeedsSheetMode, - onDeleteFeed: (Feed) -> Unit, + onFeedInfoClick: (Feed) -> Unit, onFeedSelected: (Feed) -> Unit, onFeedNameChanged: (newFeedName: String, feedLink: String) -> Unit, onFeedPinClick: (Feed) -> Unit, @@ -161,7 +149,7 @@ internal fun FeedListItem( feed = feed, isInEditMode = it, canPinFeed = canPinFeeds, - onDeleteFeed = onDeleteFeed, + onFeedInfoClick = onFeedInfoClick, onFeedPinClick = onFeedPinClick, onMarkFeedAsRead = onMarkFeedAsRead ) @@ -175,7 +163,7 @@ private fun ActionButtons( feed: Feed, isInEditMode: Boolean, canPinFeed: Boolean, - onDeleteFeed: (Feed) -> Unit, + onFeedInfoClick: (Feed) -> Unit, onFeedPinClick: (Feed) -> Unit, onMarkFeedAsRead: (Feed) -> Unit, ) { @@ -183,9 +171,9 @@ private fun ActionButtons( if (isInEditMode) { PinFeedIconButton(feed = feed, canPinFeed = canPinFeed, onFeedPinClick = onFeedPinClick) - IconButton(onClick = { onDeleteFeed(feed) }) { + IconButton(onClick = { onFeedInfoClick(feed) }) { Icon( - imageVector = TwineIcons.Delete, + imageVector = Icons.TwoTone.Info, contentDescription = null, tint = AppTheme.colorScheme.tintedForeground ) @@ -291,83 +279,3 @@ private fun PinFeedIconButton( ) } } - -@Composable -private fun FeedLabelInput( - value: String, - onFeedNameChanged: (String) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true -) { - // Maintaining local state so that it updates the text field in the UI - // instantly and doesn't have any weird UI state issues. - // - // I probably can extract this out into the presenter, we would have to - // maintain a list of text field states that are derived from the feeds list - // but this seems like a good alternative. - // - var input by remember(value) { mutableStateOf(value) } - var inputModified by remember(value) { mutableStateOf(false) } - - val focusManager = LocalFocusManager.current - val isInputBlank by derivedStateOf { input.isBlank() } - val interactionSource = remember { MutableInteractionSource() } - val isFocused by interactionSource.collectIsFocusedAsState() - - fun onFeedNameChanged(clearFocus: Boolean = true) { - inputModified = input != value - - if (!isInputBlank && inputModified) { - onFeedNameChanged.invoke(input) - } - - if (clearFocus) { - focusManager.clearFocus() - } - } - - LaunchedEffect(isFocused) { - if (!isFocused && !inputModified) { - input = value - } - } - - LaunchedEffect(input) { - // Same as setting a debounce - delay(500) - onFeedNameChanged(clearFocus = false) - } - - TextField( - modifier = modifier.requiredHeight(56.dp).fillMaxWidth(), - value = input, - onValueChange = { input = it }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, autoCorrect = false), - keyboardActions = KeyboardActions(onDone = { onFeedNameChanged() }), - singleLine = true, - textStyle = MaterialTheme.typography.titleMedium, - shape = RoundedCornerShape(16.dp), - enabled = enabled, - interactionSource = interactionSource, - colors = - TextFieldDefaults.colors( - focusedContainerColor = AppTheme.colorScheme.tintedSurface, - unfocusedContainerColor = AppTheme.colorScheme.tintedSurface, - disabledContainerColor = AppTheme.colorScheme.tintedBackground, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - disabledTextColor = AppTheme.colorScheme.textEmphasisHigh, - focusedTextColor = AppTheme.colorScheme.textEmphasisHigh, - unfocusedTextColor = AppTheme.colorScheme.textEmphasisHigh, - ), - placeholder = { - Text( - text = LocalStrings.current.feedNameHint, - style = MaterialTheme.typography.labelLarge, - color = AppTheme.colorScheme.tintedForeground.copy(alpha = 0.4f) - ) - } - ) -} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt index 33059f0c1..83681c110 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt @@ -148,7 +148,7 @@ internal fun FeedsBottomSheet( onSearchQueryChanged = { feedsPresenter.dispatch(FeedsEvent.SearchQueryChanged(it)) }, onClearSearchQuery = { feedsPresenter.dispatch(FeedsEvent.ClearSearchQuery) }, closeSheet = { feedsPresenter.dispatch(FeedsEvent.OnGoBackClicked) }, - onDeleteFeed = { feedsPresenter.dispatch(FeedsEvent.OnDeleteFeed(it)) }, + onFeedInfoClick = { feedsPresenter.dispatch(FeedsEvent.OnFeedInfoClick(it.link)) }, onFeedSelected = { feedsPresenter.dispatch(FeedsEvent.OnFeedSelected(it)) }, onFeedNameChanged = { newFeedName, feedLink -> feedsPresenter.dispatch( @@ -191,7 +191,7 @@ private fun BottomSheetExpandedContent( onSearchQueryChanged: (TextFieldValue) -> Unit, onClearSearchQuery: () -> Unit, closeSheet: () -> Unit, - onDeleteFeed: (Feed) -> Unit, + onFeedInfoClick: (Feed) -> Unit, onFeedSelected: (Feed) -> Unit, onFeedNameChanged: (newFeedName: String, feedLink: String) -> Unit, editFeeds: () -> Unit, @@ -258,7 +258,7 @@ private fun BottomSheetExpandedContent( canPinFeeds = (feed.pinnedAt != null || canPinFeeds), canShowUnreadPostsCount = canShowUnreadPostsCount, feedsSheetMode = feedsSheetMode, - onDeleteFeed = onDeleteFeed, + onFeedInfoClick = onFeedInfoClick, onFeedSelected = onFeedSelected, onFeedNameChanged = onFeedNameChanged, onFeedPinClick = onFeedPinClick, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt index 5b75d37bc..46eb7ce73 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt @@ -73,7 +73,7 @@ import me.tatarka.inject.annotations.Inject @OptIn(ExperimentalCoroutinesApi::class) class HomePresenter( dispatchersProvider: DispatchersProvider, - feedsPresenterFactory: (ComponentContext) -> FeedsPresenter, + feedsPresenterFactory: (ComponentContext, openFeedInfo: (String) -> Unit) -> FeedsPresenter, private val rssRepository: RssRepository, private val observableSelectedFeed: ObservableSelectedFeed, private val settingsRepository: SettingsRepository, @@ -82,11 +82,16 @@ class HomePresenter( @Assisted private val openBookmarks: () -> Unit, @Assisted private val openSettings: () -> Unit, @Assisted private val openPost: (postLink: String) -> Unit, + @Assisted private val openFeedInfo: (String) -> Unit, ) : ComponentContext by componentContext { private val backCallback = BackCallback { dispatch(HomeEvent.BackClicked) } - internal val feedsPresenter = feedsPresenterFactory(childContext("feeds_presenter")) + internal val feedsPresenter = + feedsPresenterFactory( + childContext("feeds_presenter"), + openFeedInfo, + ) private val presenterInstance = instanceKeeper.getOrCreate {