Skip to content

Commit

Permalink
Add option to show feed info sheet from feeds sheet (#276)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
msasikanth authored Feb 3, 2024
1 parent e89351b commit 39ca894
Show file tree
Hide file tree
Showing 17 changed files with 707 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}\"?" }
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
73 changes: 46 additions & 27 deletions shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -50,6 +56,7 @@ private typealias HomePresenterFactory =
openBookmarks: () -> Unit,
openSettings: () -> Unit,
openPost: (String) -> Unit,
openFeedInfo: (String) -> Unit,
) -> HomePresenter

private typealias SearchPresentFactory =
Expand Down Expand Up @@ -86,6 +93,13 @@ private typealias ReaderPresenterFactory =
goBack: () -> Unit,
) -> ReaderPresenter

private typealias FeedPresenterFactory =
(
feedLink: String,
ComponentContext,
dismiss: () -> Unit,
) -> FeedPresenter

@Inject
@ActivityScope
class AppPresenter(
Expand All @@ -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 {
Expand All @@ -111,6 +126,7 @@ class AppPresenter(
}

private val navigation = StackNavigation<Config>()
private val modalNavigation = SlotNavigation<ModalConfig>()

internal val screenStack: Value<ChildStack<*, Screen>> =
childStack(
Expand All @@ -120,6 +136,13 @@ class AppPresenter(
childFactory = ::createScreen,
)

internal val modalStack: Value<ChildSlot<*, Modals>> =
childSlot(
source = modalNavigation,
handleBackButton = true,
childFactory = ::createModal,
)

init {
lifecycle.doOnStart { presenterInstance.refreshFeedsIfExpired() }
}
Expand All @@ -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 -> {
Expand All @@ -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)) }
)
)
}
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
Loading

0 comments on commit 39ca894

Please sign in to comment.