Ivan Iskandar 2 роки тому
батько
коміт
bc3bb82651

+ 2 - 0
app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt

@@ -34,3 +34,5 @@ class PreferenceMutableState<T>(
         return { preference.set(it) }
     }
 }
+
+fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)

+ 2 - 2
app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt

@@ -8,9 +8,9 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 
 @Composable
-fun LoadingScreen() {
+fun LoadingScreen(modifier: Modifier = Modifier) {
     Box(
-        modifier = Modifier.fillMaxSize(),
+        modifier = modifier.fillMaxSize(),
         contentAlignment = Alignment.Center,
     ) {
         CircularProgressIndicator()

+ 43 - 73
app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt

@@ -1,110 +1,80 @@
 package eu.kanade.presentation.history
 
 import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.DeleteSweep
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ScaffoldDefaults
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import eu.kanade.domain.history.model.HistoryWithRelations
+import eu.kanade.presentation.components.AppBarTitle
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.LoadingScreen
 import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.components.SearchToolbar
 import eu.kanade.presentation.history.components.HistoryContent
-import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
-import eu.kanade.presentation.history.components.HistoryDeleteDialog
-import eu.kanade.presentation.history.components.HistoryToolbar
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.history.HistoryPresenter
-import eu.kanade.tachiyomi.ui.history.HistoryPresenter.Dialog
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
+import eu.kanade.tachiyomi.ui.history.HistoryState
 import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
-import kotlinx.coroutines.flow.collectLatest
 import java.util.Date
 
 @Composable
 fun HistoryScreen(
-    presenter: HistoryPresenter,
-    onClickCover: (HistoryWithRelations) -> Unit,
-    onClickResume: (HistoryWithRelations) -> Unit,
+    state: HistoryState,
+    snackbarHostState: SnackbarHostState,
+    incognitoMode: Boolean,
+    downloadedOnlyMode: Boolean,
+    onSearchQueryChange: (String?) -> Unit,
+    onClickCover: (mangaId: Long) -> Unit,
+    onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
+    onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
 ) {
-    val context = LocalContext.current
-
     Scaffold(
         topBar = { scrollBehavior ->
-            HistoryToolbar(
-                state = presenter,
-                incognitoMode = presenter.isIncognitoMode,
-                downloadedOnlyMode = presenter.isDownloadOnly,
+            SearchToolbar(
+                titleContent = { AppBarTitle(stringResource(R.string.history)) },
+                searchQuery = state.searchQuery,
+                onChangeSearchQuery = onSearchQueryChange,
+                actions = {
+                    IconButton(onClick = { onDialogChange(HistoryScreenModel.Dialog.DeleteAll) }) {
+                        Icon(
+                            Icons.Outlined.DeleteSweep,
+                            contentDescription = stringResource(R.string.pref_clear_history),
+                        )
+                    }
+                },
+                downloadedOnlyMode = downloadedOnlyMode,
+                incognitoMode = incognitoMode,
                 scrollBehavior = scrollBehavior,
             )
         },
+        snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+        contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
     ) { contentPadding ->
-        val items by presenter.getHistory().collectAsState(initial = null)
-        val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
-        items.let {
+        state.list.let {
             if (it == null) {
-                LoadingScreen()
+                LoadingScreen(modifier = Modifier.padding(contentPadding))
             } else if (it.isEmpty()) {
                 EmptyScreen(
                     textResource = R.string.information_no_recent_manga,
-                    modifier = Modifier.padding(contentPaddingWithNavBar),
+                    modifier = Modifier.padding(contentPadding),
                 )
             } else {
                 HistoryContent(
                     history = it,
-                    contentPadding = contentPaddingWithNavBar,
-                    onClickCover = onClickCover,
-                    onClickResume = onClickResume,
-                    onClickDelete = { item -> presenter.dialog = Dialog.Delete(item) },
+                    contentPadding = contentPadding,
+                    onClickCover = { history -> onClickCover(history.mangaId) },
+                    onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
+                    onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
                 )
             }
         }
-
-        LaunchedEffect(items) {
-            if (items != null) {
-                (presenter.view?.activity as? MainActivity)?.ready = true
-            }
-        }
-    }
-    val onDismissRequest = { presenter.dialog = null }
-    when (val dialog = presenter.dialog) {
-        is Dialog.Delete -> {
-            HistoryDeleteDialog(
-                onDismissRequest = onDismissRequest,
-                onDelete = { all ->
-                    if (all) {
-                        presenter.removeAllFromHistory(dialog.history.mangaId)
-                    } else {
-                        presenter.removeFromHistory(dialog.history)
-                    }
-                },
-            )
-        }
-        is Dialog.DeleteAll -> {
-            HistoryDeleteAllDialog(
-                onDismissRequest = onDismissRequest,
-                onDelete = {
-                    presenter.removeAllHistory()
-                },
-            )
-        }
-        null -> {}
-    }
-    LaunchedEffect(Unit) {
-        presenter.events.collectLatest { event ->
-            when (event) {
-                HistoryPresenter.Event.InternalError -> context.toast(R.string.internal_error)
-                HistoryPresenter.Event.NoNextChapterFound -> context.toast(R.string.no_next_chapter)
-                is HistoryPresenter.Event.OpenChapter -> {
-                    val intent = ReaderActivity.newIntent(context, event.chapter.mangaId, event.chapter.id)
-                    context.startActivity(intent)
-                }
-            }
-        }
     }
 }
 

+ 0 - 36
app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt

@@ -1,36 +0,0 @@
-package eu.kanade.presentation.history.components
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.DeleteSweep
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import eu.kanade.presentation.components.AppBarTitle
-import eu.kanade.presentation.components.SearchToolbar
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.history.HistoryPresenter
-import eu.kanade.tachiyomi.ui.history.HistoryState
-
-@Composable
-fun HistoryToolbar(
-    state: HistoryState,
-    scrollBehavior: TopAppBarScrollBehavior,
-    incognitoMode: Boolean,
-    downloadedOnlyMode: Boolean,
-) {
-    SearchToolbar(
-        titleContent = { AppBarTitle(stringResource(R.string.history)) },
-        searchQuery = state.searchQuery,
-        onChangeSearchQuery = { state.searchQuery = it },
-        actions = {
-            IconButton(onClick = { state.dialog = HistoryPresenter.Dialog.DeleteAll }) {
-                Icon(Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.pref_clear_history))
-            }
-        },
-        downloadedOnlyMode = downloadedOnlyMode,
-        incognitoMode = incognitoMode,
-        scrollBehavior = scrollBehavior,
-    )
-}

+ 8 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt

@@ -5,6 +5,8 @@ import android.view.LayoutInflater
 import android.view.View
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
 import eu.kanade.tachiyomi.util.view.setComposeContent
 import nucleus.presenter.Presenter
@@ -21,7 +23,9 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
 
         binding.root.apply {
             setComposeContent {
-                ComposeContent()
+                CompositionLocalProvider(LocalRouter provides router) {
+                    ComposeContent()
+                }
             }
         }
     }
@@ -52,7 +56,9 @@ abstract class BasicFullComposeController(bundle: Bundle? = null) :
 
         binding.root.apply {
             setComposeContent {
-                ComposeContent()
+                CompositionLocalProvider(LocalRouter provides router) {
+                    ComposeContent()
+                }
             }
         }
     }

+ 13 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt

@@ -1,30 +1,26 @@
 package eu.kanade.tachiyomi.ui.history
 
 import androidx.compose.runtime.Composable
-import eu.kanade.presentation.history.HistoryScreen
-import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
+import cafe.adriel.voyager.navigator.Navigator
+import eu.kanade.domain.history.interactor.GetNextChapters
+import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.lang.launchIO
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
-class HistoryController : FullComposeController<HistoryPresenter>(), RootController {
-
-    override fun createPresenter() = HistoryPresenter()
+class HistoryController : BasicFullComposeController(), RootController {
 
     @Composable
     override fun ComposeContent() {
-        HistoryScreen(
-            presenter = presenter,
-            onClickCover = { history ->
-                router.pushController(MangaController(history.mangaId))
-            },
-            onClickResume = { history ->
-                presenter.getNextChapterForManga(history.mangaId, history.chapterId)
-            },
-        )
+        Navigator(screen = HistoryScreen)
     }
 
     fun resumeLastChapterRead() {
-        presenter.resumeLastChapterRead()
+        val context = activity ?: return
+        viewScope.launchIO {
+            val chapter = Injekt.get<GetNextChapters>().await(onlyUnread = false).firstOrNull()
+            HistoryScreen.openChapter(context, chapter)
+        }
     }
 }

+ 97 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt

@@ -0,0 +1,97 @@
+package eu.kanade.tachiyomi.ui.history
+
+import android.content.Context
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalContext
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.currentOrThrow
+import eu.kanade.domain.chapter.model.Chapter
+import eu.kanade.presentation.history.HistoryScreen
+import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
+import eu.kanade.presentation.history.components.HistoryDeleteDialog
+import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.pushController
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import kotlinx.coroutines.flow.collectLatest
+
+object HistoryScreen : Screen {
+
+    private val snackbarHostState = SnackbarHostState()
+
+    @Composable
+    override fun Content() {
+        val router = LocalRouter.currentOrThrow
+        val context = LocalContext.current
+        val screenModel = rememberScreenModel { HistoryScreenModel() }
+        val state by screenModel.state.collectAsState()
+
+        HistoryScreen(
+            state = state,
+            snackbarHostState = snackbarHostState,
+            incognitoMode = screenModel.isIncognitoMode,
+            downloadedOnlyMode = screenModel.isDownloadOnly,
+            onSearchQueryChange = screenModel::updateSearchQuery,
+            onClickCover = { router.pushController(MangaController(it)) },
+            onClickResume = screenModel::getNextChapterForManga,
+            onDialogChange = screenModel::setDialog,
+        )
+
+        val onDismissRequest = { screenModel.setDialog(null) }
+        when (val dialog = state.dialog) {
+            is HistoryScreenModel.Dialog.Delete -> {
+                HistoryDeleteDialog(
+                    onDismissRequest = onDismissRequest,
+                    onDelete = { all ->
+                        if (all) {
+                            screenModel.removeAllFromHistory(dialog.history.mangaId)
+                        } else {
+                            screenModel.removeFromHistory(dialog.history)
+                        }
+                    },
+                )
+            }
+            is HistoryScreenModel.Dialog.DeleteAll -> {
+                HistoryDeleteAllDialog(
+                    onDismissRequest = onDismissRequest,
+                    onDelete = screenModel::removeAllHistory,
+                )
+            }
+            null -> {}
+        }
+
+        LaunchedEffect(state.list) {
+            if (state.list != null) {
+                (context as? MainActivity)?.ready = true
+            }
+        }
+
+        LaunchedEffect(Unit) {
+            screenModel.events.collectLatest { e ->
+                when (e) {
+                    HistoryScreenModel.Event.InternalError ->
+                        snackbarHostState.showSnackbar(context.getString(R.string.internal_error))
+                    HistoryScreenModel.Event.HistoryCleared ->
+                        snackbarHostState.showSnackbar(context.getString(R.string.clear_history_completed))
+                    is HistoryScreenModel.Event.OpenChapter -> openChapter(context, e.chapter)
+                }
+            }
+        }
+    }
+
+    suspend fun openChapter(context: Context, chapter: Chapter?) {
+        if (chapter != null) {
+            val intent = ReaderActivity.newIntent(context, chapter.mangaId, chapter.id)
+            context.startActivity(intent)
+        } else {
+            snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter))
+        }
+    }
+}

+ 49 - 56
app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryPresenter.kt → app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt

@@ -1,11 +1,10 @@
 package eu.kanade.tachiyomi.ui.history
 
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.coroutineScope
+import eu.kanade.core.prefs.asState
 import eu.kanade.core.util.insertSeparators
 import eu.kanade.domain.base.BasePreferences
 import eu.kanade.domain.chapter.model.Chapter
@@ -14,51 +13,53 @@ import eu.kanade.domain.history.interactor.GetNextChapters
 import eu.kanade.domain.history.interactor.RemoveHistory
 import eu.kanade.domain.history.model.HistoryWithRelations
 import eu.kanade.presentation.history.HistoryUiModel
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.toDateKey
-import eu.kanade.tachiyomi.util.lang.withUIContext
 import eu.kanade.tachiyomi.util.system.logcat
-import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
 import logcat.LogPriority
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import java.util.Date
 
-class HistoryPresenter(
-    private val state: HistoryStateImpl = HistoryState() as HistoryStateImpl,
+class HistoryScreenModel(
     private val getHistory: GetHistory = Injekt.get(),
     private val getNextChapters: GetNextChapters = Injekt.get(),
     private val removeHistory: RemoveHistory = Injekt.get(),
     preferences: BasePreferences = Injekt.get(),
-) : BasePresenter<HistoryController>(), HistoryState by state {
+) : StateScreenModel<HistoryState>(HistoryState()) {
 
-    private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
+    private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
     val events: Flow<Event> = _events.receiveAsFlow()
 
-    val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
-    val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
+    val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
+    val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
 
-    @Composable
-    fun getHistory(): Flow<List<HistoryUiModel>> {
-        val query = searchQuery ?: ""
-        return remember(query) {
-            getHistory.subscribe(query)
+    init {
+        coroutineScope.launch {
+            state.map { it.searchQuery }
                 .distinctUntilChanged()
-                .catch { error ->
-                    logcat(LogPriority.ERROR, error)
-                    _events.send(Event.InternalError)
-                }
-                .map { pagingData ->
-                    pagingData.toHistoryUiModels()
+                .flatMapLatest { query ->
+                    getHistory.subscribe(query ?: "")
+                        .distinctUntilChanged()
+                        .catch { error ->
+                            logcat(LogPriority.ERROR, error)
+                            _events.send(Event.InternalError)
+                        }
+                        .map { it.toHistoryUiModels() }
+                        .flowOn(Dispatchers.IO)
                 }
+                .collect { newList -> mutableState.update { it.copy(list = newList) } }
         }
     }
 
@@ -76,67 +77,59 @@ class HistoryPresenter(
     }
 
     fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
-        presenterScope.launchIO {
+        coroutineScope.launchIO {
             sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
         }
     }
 
-    fun resumeLastChapterRead() {
-        presenterScope.launchIO {
-            sendNextChapterEvent(getNextChapters.await(onlyUnread = false))
-        }
-    }
-
     private suspend fun sendNextChapterEvent(chapters: List<Chapter>) {
         val chapter = chapters.firstOrNull()
-        _events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
+        _events.send(Event.OpenChapter(chapter))
     }
 
     fun removeFromHistory(history: HistoryWithRelations) {
-        presenterScope.launchIO {
+        coroutineScope.launchIO {
             removeHistory.await(history)
         }
     }
 
     fun removeAllFromHistory(mangaId: Long) {
-        presenterScope.launchIO {
+        coroutineScope.launchIO {
             removeHistory.await(mangaId)
         }
     }
 
     fun removeAllHistory() {
-        presenterScope.launchIO {
+        coroutineScope.launchIO {
             val result = removeHistory.awaitAll()
             if (!result) return@launchIO
-            withUIContext {
-                view?.activity?.toast(R.string.clear_history_completed)
-            }
+            _events.send(Event.HistoryCleared)
         }
     }
 
+    fun updateSearchQuery(query: String?) {
+        mutableState.update { it.copy(searchQuery = query) }
+    }
+
+    fun setDialog(dialog: Dialog?) {
+        mutableState.update { it.copy(dialog = dialog) }
+    }
+
     sealed class Dialog {
         object DeleteAll : Dialog()
         data class Delete(val history: HistoryWithRelations) : Dialog()
     }
 
     sealed class Event {
+        data class OpenChapter(val chapter: Chapter?) : Event()
         object InternalError : Event()
-        object NoNextChapterFound : Event()
-        data class OpenChapter(val chapter: Chapter) : Event()
+        object HistoryCleared : Event()
     }
 }
 
-@Stable
-interface HistoryState {
-    var searchQuery: String?
-    var dialog: HistoryPresenter.Dialog?
-}
-
-fun HistoryState(): HistoryState {
-    return HistoryStateImpl()
-}
-
-class HistoryStateImpl : HistoryState {
-    override var searchQuery: String? by mutableStateOf(null)
-    override var dialog: HistoryPresenter.Dialog? by mutableStateOf(null)
-}
+@Immutable
+data class HistoryState(
+    val searchQuery: String? = null,
+    val list: List<HistoryUiModel>? = null,
+    val dialog: HistoryScreenModel.Dialog? = null,
+)

+ 19 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt

@@ -9,6 +9,7 @@ import android.os.Parcelable
 import android.util.AttributeSet
 import android.view.ViewPropertyAnimator
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.calculateEndPadding
 import androidx.compose.foundation.layout.calculateStartPadding
 import androidx.compose.runtime.Composable
@@ -16,6 +17,7 @@ import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.max
@@ -26,6 +28,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
 import eu.kanade.tachiyomi.util.system.pxToDp
+import kotlin.math.max
 
 class TachiyomiBottomNavigationView @JvmOverloads constructor(
     context: Context,
@@ -173,5 +176,21 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
                 bottom = max(origin.calculateBottomPadding(), bottomNavPadding),
             )
         }
+
+        /**
+         * @see withBottomNavPadding
+         */
+        @ReadOnlyComposable
+        @Composable
+        fun withBottomNavInset(origin: WindowInsets): WindowInsets {
+            val density = LocalDensity.current
+            val layoutDirection = LocalLayoutDirection.current
+            return WindowInsets(
+                left = origin.getLeft(density, layoutDirection),
+                top = origin.getTop(density),
+                right = origin.getRight(density, layoutDirection),
+                bottom = max(origin.getBottom(density), with(density) { bottomNavPadding.roundToPx() }),
+            )
+        }
     }
 }