Преглед на файлове

Move more components to presentation-core module

arkon преди 2 години
родител
ревизия
10d7349506
променени са 51 файла, в които са добавени 487 реда и са изтрити 475 реда
  1. 0 12
      app/src/main/java/eu/kanade/core/navigation/Screen.kt
  2. 3 3
      app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt
  3. 1 1
      app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
  4. 1 1
      app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt
  5. 2 2
      app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt
  6. 1 1
      app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt
  7. 2 2
      app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt
  8. 1 1
      app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt
  9. 2 2
      app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt
  10. 1 1
      app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt
  11. 2 252
      app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt
  12. 2 130
      app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt
  13. 2 2
      app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt
  14. 2 2
      app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
  15. 1 1
      app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt
  16. 2 2
      app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt
  17. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt
  18. 3 3
      app/src/main/java/eu/kanade/presentation/more/settings/screen/ClearDatabaseScreen.kt
  19. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt
  20. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt
  21. 2 2
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt
  22. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/screen/WorkerInfoScreen.kt
  23. 2 2
      app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt
  24. 2 2
      app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
  25. 10 0
      app/src/main/java/eu/kanade/presentation/util/Navigator.kt
  26. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt
  27. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt
  28. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt
  29. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt
  30. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt
  31. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt
  32. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
  33. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt
  34. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
  35. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt
  36. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt
  37. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt
  38. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
  39. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
  40. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
  41. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt
  42. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt
  43. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt
  44. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt
  45. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt
  46. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt
  47. 1 0
      presentation-core/build.gradle.kts
  48. 261 0
      presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt
  49. 133 0
      presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt
  50. 12 15
      presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt
  51. 1 1
      presentation-core/src/main/java/tachiyomi/presentation/core/screens/LoadingScreen.kt

+ 0 - 12
app/src/main/java/eu/kanade/core/navigation/Screen.kt

@@ -1,12 +0,0 @@
-package eu.kanade.core.navigation
-
-import cafe.adriel.voyager.core.screen.ScreenKey
-import cafe.adriel.voyager.core.screen.uniqueScreenKey
-import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen
-
-// TODO: this prevents crashes in nested navigators with transitions not being disposed
-// properly. Go back to using vanilla Voyager Screens once fixed upstream.
-abstract class Screen : VoyagerScreen {
-
-    override val key: ScreenKey = uniqueScreenKey
-}

+ 3 - 3
app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt

@@ -21,8 +21,6 @@ import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
 import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
 import eu.kanade.presentation.browse.components.BrowseSourceList
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
-import eu.kanade.presentation.components.EmptyScreenAction
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.source.Source
@@ -30,8 +28,10 @@ import eu.kanade.tachiyomi.source.SourceManager
 import kotlinx.coroutines.flow.StateFlow
 import tachiyomi.domain.library.model.LibraryDisplayMode
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.EmptyScreenAction
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 @Composable
 fun BrowseSourceContent(

+ 1 - 1
app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt

@@ -47,7 +47,6 @@ import eu.kanade.domain.extension.interactor.ExtensionSourceItem
 import eu.kanade.presentation.browse.components.ExtensionIcon
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.AppBarActions
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.WarningBanner
 import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
@@ -61,6 +60,7 @@ import tachiyomi.presentation.core.components.material.DIVIDER_ALPHA
 import tachiyomi.presentation.core.components.material.Divider
 import tachiyomi.presentation.core.components.material.Scaffold
 import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.screens.EmptyScreen
 
 @Composable
 fun ExtensionDetailsScreen(

+ 1 - 1
app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt

@@ -8,13 +8,13 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
 
 @Composable
 fun ExtensionFilterScreen(

+ 2 - 2
app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt

@@ -37,7 +37,6 @@ import androidx.compose.ui.unit.dp
 import com.google.accompanist.flowlayout.FlowRow
 import eu.kanade.presentation.browse.components.BaseBrowseItem
 import eu.kanade.presentation.browse.components.ExtensionIcon
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.extension.model.Extension
@@ -46,10 +45,11 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
 import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.PullRefresh
 import tachiyomi.presentation.core.components.material.padding
 import tachiyomi.presentation.core.components.material.topSmallPaddingValues
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import tachiyomi.presentation.core.theme.header
 import tachiyomi.presentation.core.util.plus
 import tachiyomi.presentation.core.util.secondaryItemAlpha

+ 1 - 1
app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt

@@ -6,13 +6,13 @@ import androidx.compose.foundation.lazy.items
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.manga.components.BaseMangaListItem
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
 
 @Composable
 fun MigrateMangaScreen(

+ 2 - 2
app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt

@@ -25,18 +25,18 @@ import androidx.compose.ui.text.style.TextOverflow
 import eu.kanade.domain.source.interactor.SetMigrateSorting
 import eu.kanade.presentation.browse.components.BaseSourceItem
 import eu.kanade.presentation.browse.components.SourceIcon
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
 import eu.kanade.tachiyomi.util.system.copyToClipboard
 import tachiyomi.domain.source.model.Source
 import tachiyomi.presentation.core.components.Badge
 import tachiyomi.presentation.core.components.BadgeGroup
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
 import tachiyomi.presentation.core.components.material.padding
 import tachiyomi.presentation.core.components.material.topSmallPaddingValues
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import tachiyomi.presentation.core.theme.header
 import tachiyomi.presentation.core.util.plus
 import tachiyomi.presentation.core.util.secondaryItemAlpha

+ 1 - 1
app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt

@@ -10,7 +10,6 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import eu.kanade.presentation.browse.components.BaseSourceItem
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState
@@ -18,6 +17,7 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
 import tachiyomi.domain.source.model.Source
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
 
 @Composable
 fun SourcesFilterScreen(

+ 2 - 2
app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt

@@ -22,7 +22,6 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import eu.kanade.presentation.browse.components.BaseSourceItem
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.ui.browse.source.SourcesState
@@ -30,10 +29,11 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listi
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import tachiyomi.domain.source.model.Pin
 import tachiyomi.domain.source.model.Source
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 import tachiyomi.presentation.core.components.material.padding
 import tachiyomi.presentation.core.components.material.topSmallPaddingValues
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import tachiyomi.presentation.core.theme.header
 import tachiyomi.presentation.core.util.plus
 

+ 1 - 1
app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt

@@ -10,13 +10,13 @@ import androidx.compose.ui.res.stringResource
 import eu.kanade.presentation.category.components.CategoryContent
 import eu.kanade.presentation.category.components.CategoryFloatingActionButton
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.category.CategoryScreenState
 import tachiyomi.domain.category.model.Category
 import tachiyomi.presentation.core.components.material.Scaffold
 import tachiyomi.presentation.core.components.material.padding
 import tachiyomi.presentation.core.components.material.topSmallPaddingValues
+import tachiyomi.presentation.core.screens.EmptyScreen
 import tachiyomi.presentation.core.util.plus
 
 @Composable

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

@@ -1,74 +1,25 @@
 package eu.kanade.presentation.components
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.with
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredWidthIn
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.systemBarsPadding
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.foundation.shape.ZeroCornerSize
-import androidx.compose.material.SwipeableState
-import androidx.compose.material.rememberSwipeableState
-import androidx.compose.material.swipeable
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
+import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.transitions.ScreenTransition
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.util.isTabletUi
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
-import kotlin.time.Duration.Companion.milliseconds
-
-private const val SheetAnimationDuration = 500
-private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
-private const val ScrimAnimationDuration = 350
-private val ScrimAnimationSpec = tween<Float>(durationMillis = ScrimAnimationDuration)
+import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
 
 @Composable
 fun NavigatorAdaptiveSheet(
@@ -141,204 +92,3 @@ fun AdaptiveSheet(
         content(contentPadding)
     }
 }
-
-@Composable
-fun AdaptiveSheetImpl(
-    isTabletUi: Boolean,
-    tonalElevation: Dp,
-    enableSwipeDismiss: Boolean,
-    onDismissRequest: () -> Unit,
-    content: @Composable () -> Unit,
-) {
-    val scope = rememberCoroutineScope()
-    if (isTabletUi) {
-        var targetAlpha by remember { mutableStateOf(0f) }
-        val alpha by animateFloatAsState(
-            targetValue = targetAlpha,
-            animationSpec = ScrimAnimationSpec,
-        )
-        val internalOnDismissRequest: () -> Unit = {
-            scope.launch {
-                targetAlpha = 0f
-                delay(ScrimAnimationSpec.durationMillis.milliseconds)
-                onDismissRequest()
-            }
-        }
-        BoxWithConstraints(
-            modifier = Modifier
-                .clickable(
-                    enabled = true,
-                    interactionSource = remember { MutableInteractionSource() },
-                    indication = null,
-                    onClick = internalOnDismissRequest,
-                )
-                .fillMaxSize()
-                .alpha(alpha),
-            contentAlignment = Alignment.Center,
-        ) {
-            Box(
-                modifier = Modifier
-                    .matchParentSize()
-                    .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
-            )
-            Surface(
-                modifier = Modifier
-                    .requiredWidthIn(max = 460.dp)
-                    .clickable(
-                        interactionSource = remember { MutableInteractionSource() },
-                        indication = null,
-                        onClick = {},
-                    )
-                    .systemBarsPadding()
-                    .padding(vertical = 16.dp),
-                shape = MaterialTheme.shapes.extraLarge,
-                tonalElevation = tonalElevation,
-                content = {
-                    BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
-                    content()
-                },
-            )
-
-            LaunchedEffect(Unit) {
-                targetAlpha = 1f
-            }
-        }
-    } else {
-        val swipeState = rememberSwipeableState(
-            initialValue = 1,
-            animationSpec = SheetAnimationSpec,
-        )
-        val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
-        BoxWithConstraints(
-            modifier = Modifier
-                .clickable(
-                    interactionSource = remember { MutableInteractionSource() },
-                    indication = null,
-                    onClick = internalOnDismissRequest,
-                )
-                .fillMaxSize(),
-            contentAlignment = Alignment.BottomCenter,
-        ) {
-            val fullHeight = constraints.maxHeight.toFloat()
-            val anchors = mapOf(0f to 0, fullHeight to 1)
-            val scrimAlpha by animateFloatAsState(
-                targetValue = if (swipeState.targetValue == 1) 0f else 1f,
-                animationSpec = ScrimAnimationSpec,
-            )
-            Box(
-                modifier = Modifier
-                    .matchParentSize()
-                    .alpha(scrimAlpha)
-                    .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
-            )
-            Surface(
-                modifier = Modifier
-                    .widthIn(max = 460.dp)
-                    .clickable(
-                        interactionSource = remember { MutableInteractionSource() },
-                        indication = null,
-                        onClick = {},
-                    )
-                    .nestedScroll(
-                        remember(enableSwipeDismiss, anchors) {
-                            swipeState.preUpPostDownNestedScrollConnection(
-                                enabled = enableSwipeDismiss,
-                                anchor = anchors,
-                            )
-                        },
-                    )
-                    .offset {
-                        IntOffset(
-                            0,
-                            swipeState.offset.value.roundToInt(),
-                        )
-                    }
-                    .swipeable(
-                        enabled = enableSwipeDismiss,
-                        state = swipeState,
-                        anchors = anchors,
-                        orientation = Orientation.Vertical,
-                        resistance = null,
-                    )
-                    .windowInsetsPadding(
-                        WindowInsets.systemBars
-                            .only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
-                    )
-                    .consumeWindowInsets(
-                        WindowInsets.systemBars
-                            .only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
-                    ),
-                shape = MaterialTheme.shapes.extraLarge.copy(bottomStart = ZeroCornerSize, bottomEnd = ZeroCornerSize),
-                tonalElevation = tonalElevation,
-                content = {
-                    BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
-                    content()
-                },
-            )
-
-            LaunchedEffect(swipeState) {
-                scope.launch { swipeState.animateTo(0) }
-                snapshotFlow { swipeState.currentValue }
-                    .drop(1)
-                    .filter { it == 1 }
-                    .collectLatest {
-                        delay(ScrimAnimationSpec.durationMillis.milliseconds)
-                        onDismissRequest()
-                    }
-            }
-        }
-    }
-}
-
-/**
- * Yoinked from Swipeable.kt with modifications to disable
- */
-private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
-    enabled: Boolean = true,
-    anchor: Map<Float, T>,
-) = object : NestedScrollConnection {
-    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
-        val delta = available.toFloat()
-        return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
-            performDrag(delta).toOffset()
-        } else {
-            Offset.Zero
-        }
-    }
-
-    override fun onPostScroll(
-        consumed: Offset,
-        available: Offset,
-        source: NestedScrollSource,
-    ): Offset {
-        return if (enabled && source == NestedScrollSource.Drag) {
-            performDrag(available.toFloat()).toOffset()
-        } else {
-            Offset.Zero
-        }
-    }
-
-    override suspend fun onPreFling(available: Velocity): Velocity {
-        val toFling = Offset(available.x, available.y).toFloat()
-        return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
-            performFling(velocity = toFling)
-            // since we go to the anchor with tween settling, consume all for the best UX
-            available
-        } else {
-            Velocity.Zero
-        }
-    }
-
-    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-        return if (enabled) {
-            performFling(velocity = Offset(available.x, available.y).toFloat())
-            available
-        } else {
-            Velocity.Zero
-        }
-    }
-
-    private fun Float.toOffset(): Offset = Offset(0f, this)
-
-    private fun Offset.toFloat(): Float = this.y
-}

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

@@ -1,124 +1,15 @@
 package eu.kanade.presentation.components
 
-import androidx.annotation.StringRes
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.HelpOutline
 import androidx.compose.material.icons.outlined.Refresh
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
 import eu.kanade.presentation.theme.TachiyomiTheme
 import eu.kanade.tachiyomi.R
-import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.EmptyScreenAction
 import tachiyomi.presentation.core.util.ThemePreviews
-import tachiyomi.presentation.core.util.secondaryItemAlpha
-import kotlin.random.Random
-
-@Composable
-fun EmptyScreen(
-    @StringRes textResource: Int,
-    modifier: Modifier = Modifier,
-    actions: List<EmptyScreenAction>? = null,
-) {
-    EmptyScreen(
-        message = stringResource(textResource),
-        modifier = modifier,
-        actions = actions,
-    )
-}
-
-@Composable
-fun EmptyScreen(
-    message: String,
-    modifier: Modifier = Modifier,
-    actions: List<EmptyScreenAction>? = null,
-) {
-    val face = remember { getRandomErrorFace() }
-    Column(
-        modifier = modifier
-            .fillMaxSize()
-            .padding(horizontal = 24.dp),
-        horizontalAlignment = Alignment.CenterHorizontally,
-        verticalArrangement = Arrangement.Center,
-    ) {
-        Text(
-            text = face,
-            modifier = Modifier.secondaryItemAlpha(),
-            style = MaterialTheme.typography.displayMedium,
-        )
-
-        Text(
-            text = message,
-            modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(),
-            style = MaterialTheme.typography.bodyMedium,
-            textAlign = TextAlign.Center,
-        )
-
-        if (!actions.isNullOrEmpty()) {
-            Row(
-                modifier = Modifier
-                    .padding(
-                        top = 24.dp,
-                        start = 24.dp,
-                        end = 24.dp,
-                    ),
-                horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
-            ) {
-                actions.forEach {
-                    ActionButton(
-                        modifier = Modifier.weight(1f),
-                        title = stringResource(it.stringResId),
-                        icon = it.icon,
-                        onClick = it.onClick,
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-private fun ActionButton(
-    modifier: Modifier = Modifier,
-    title: String,
-    icon: ImageVector,
-    onClick: () -> Unit,
-) {
-    TextButton(
-        modifier = modifier,
-        onClick = onClick,
-    ) {
-        Column(horizontalAlignment = Alignment.CenterHorizontally) {
-            Icon(
-                imageVector = icon,
-                contentDescription = null,
-            )
-            Spacer(Modifier.height(4.dp))
-            Text(
-                text = title,
-                textAlign = TextAlign.Center,
-            )
-        }
-    }
-}
 
 @ThemePreviews
 @Composable
@@ -155,22 +46,3 @@ private fun WithActionPreview() {
         }
     }
 }
-
-data class EmptyScreenAction(
-    @StringRes val stringResId: Int,
-    val icon: ImageVector,
-    val onClick: () -> Unit,
-)
-
-private val ERROR_FACES = listOf(
-    "(・o・;)",
-    "Σ(ಠ_ಠ)",
-    "ಥ_ಥ",
-    "(˘・_・˘)",
-    "(; ̄Д ̄)",
-    "(・Д・。",
-)
-
-private fun getRandomErrorFace(): String {
-    return ERROR_FACES[Random.nextInt(ERROR_FACES.size)]
-}

+ 2 - 2
app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt

@@ -14,12 +14,12 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
-import eu.kanade.presentation.components.InfoScaffold
 import eu.kanade.presentation.theme.TachiyomiTheme
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.CrashLogUtil
 import kotlinx.coroutines.launch
 import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.screens.InfoScreen
 import tachiyomi.presentation.core.util.ThemePreviews
 
 @Composable
@@ -30,7 +30,7 @@ fun CrashScreen(
     val scope = rememberCoroutineScope()
     val context = LocalContext.current
 
-    InfoScaffold(
+    InfoScreen(
         icon = Icons.Outlined.BugReport,
         headingText = stringResource(R.string.crash_screen_title),
         subtitleText = stringResource(R.string.crash_screen_description, stringResource(R.string.app_name)),

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

@@ -11,15 +11,15 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import eu.kanade.presentation.components.AppBarTitle
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.SearchToolbar
 import eu.kanade.presentation.history.components.HistoryContent
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
 import eu.kanade.tachiyomi.ui.history.HistoryState
 import tachiyomi.domain.history.model.HistoryWithRelations
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import java.util.Date
 
 @Composable

+ 1 - 1
app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt

@@ -17,13 +17,13 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.dp
 import eu.kanade.core.prefs.PreferenceMutableState
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.library.LibraryItem
 import tachiyomi.domain.library.model.LibraryDisplayMode
 import tachiyomi.domain.library.model.LibraryManga
 import tachiyomi.presentation.core.components.HorizontalPager
 import tachiyomi.presentation.core.components.PagerState
+import tachiyomi.presentation.core.screens.EmptyScreen
 import tachiyomi.presentation.core.util.plus
 
 @Composable

+ 2 - 2
app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt

@@ -19,10 +19,10 @@ import com.halilibo.richtext.markdown.Markdown
 import com.halilibo.richtext.ui.RichTextStyle
 import com.halilibo.richtext.ui.material3.Material3RichText
 import com.halilibo.richtext.ui.string.RichTextStringStyle
-import eu.kanade.presentation.components.InfoScaffold
 import eu.kanade.presentation.theme.TachiyomiTheme
 import eu.kanade.tachiyomi.R
 import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.screens.InfoScreen
 import tachiyomi.presentation.core.util.ThemePreviews
 
 @Composable
@@ -33,7 +33,7 @@ fun NewUpdateScreen(
     onRejectUpdate: () -> Unit,
     onAcceptUpdate: () -> Unit,
 ) {
-    InfoScaffold(
+    InfoScreen(
         icon = Icons.Outlined.NewReleases,
         headingText = stringResource(R.string.update_check_notification_update_available),
         subtitleText = versionName,

+ 1 - 1
app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt

@@ -18,12 +18,12 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.domain.ui.UiPreferences
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.more.LogoHeader
 import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.presentation.util.LocalBackPress
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.updater.AppUpdateChecker

+ 3 - 3
app/src/main/java/eu/kanade/presentation/more/settings/screen/ClearDatabaseScreen.kt

@@ -32,12 +32,11 @@ import cafe.adriel.voyager.core.model.coroutineScope
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
 import eu.kanade.presentation.browse.components.SourceIcon
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.AppBarActions
-import eu.kanade.presentation.components.EmptyScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.flow.collectLatest
@@ -49,9 +48,10 @@ import tachiyomi.data.Database
 import tachiyomi.domain.source.model.Source
 import tachiyomi.domain.source.model.SourceWithCount
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.Divider
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import tachiyomi.presentation.core.util.selectedBackground
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get

+ 1 - 1
app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt

@@ -9,8 +9,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
 import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import tachiyomi.presentation.core.components.material.Scaffold
 

+ 1 - 1
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt

@@ -44,11 +44,11 @@ import androidx.core.graphics.ColorUtils
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.AppBarActions
 import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.presentation.util.LocalBackPress
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import tachiyomi.presentation.core.components.LazyColumn
 import tachiyomi.presentation.core.components.material.Scaffold

+ 2 - 2
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt

@@ -49,13 +49,13 @@ import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.more.settings.Preference
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.isLTR
 import tachiyomi.presentation.core.components.material.Divider
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
 import tachiyomi.presentation.core.util.runOnEnterKeyPressed
 import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen
 

+ 1 - 1
app/src/main/java/eu/kanade/presentation/more/settings/screen/WorkerInfoScreen.kt

@@ -36,7 +36,7 @@ import cafe.adriel.voyager.core.model.coroutineScope
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.map

+ 2 - 2
app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt

@@ -56,15 +56,15 @@ import androidx.compose.ui.text.intl.Locale
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.toLowerCase
 import androidx.compose.ui.unit.dp
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.manga.components.MangaCover
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 import tachiyomi.presentation.core.components.material.Divider
 import tachiyomi.presentation.core.components.material.Scaffold
 import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import tachiyomi.presentation.core.util.plus
 import tachiyomi.presentation.core.util.runOnEnterKeyPressed
 import tachiyomi.presentation.core.util.secondaryItemAlpha

+ 2 - 2
app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt

@@ -24,7 +24,6 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastAny
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.manga.components.ChapterDownloadAction
 import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 import eu.kanade.tachiyomi.R
@@ -34,9 +33,10 @@ import eu.kanade.tachiyomi.ui.updates.UpdatesState
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.PullRefresh
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import kotlin.time.Duration.Companion.seconds
 
 @Composable

+ 10 - 0
app/src/main/java/eu/kanade/presentation/util/Navigator.kt

@@ -3,6 +3,9 @@ package eu.kanade.presentation.util
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ProvidableCompositionLocal
 import androidx.compose.runtime.staticCompositionLocalOf
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
 import cafe.adriel.voyager.core.stack.StackEvent
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.transitions.ScreenTransition
@@ -18,6 +21,13 @@ interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
     suspend fun onReselect(navigator: Navigator) {}
 }
 
+// TODO: this prevents crashes in nested navigators with transitions not being disposed
+// properly. Go back to using vanilla Voyager Screens once fixed upstream.
+abstract class Screen : Screen {
+
+    override val key: ScreenKey = uniqueScreenKey
+}
+
 interface AssistContentScreen {
     fun onProvideAssistUrl(): String?
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt

@@ -8,12 +8,12 @@ import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.ExtensionFilterScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.flow.collectLatest
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 class ExtensionFilterScreen : Screen() {
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt

@@ -9,10 +9,10 @@ import androidx.compose.ui.platform.LocalUriHandler
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.ExtensionDetailsScreen
+import eu.kanade.presentation.util.Screen
 import kotlinx.coroutines.flow.collectLatest
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 data class ExtensionDetailsScreen(
     private val pkgName: String,

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt

@@ -37,7 +37,7 @@ import androidx.preference.forEach
 import androidx.preference.getOnBindEditTextListener
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
 import eu.kanade.tachiyomi.source.ConfigurableSource

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt

@@ -8,14 +8,14 @@ import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.MigrateMangaScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.flow.collectLatest
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 data class MigrationMangaScreen(
     private val sourceId: Long,

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt

@@ -51,7 +51,7 @@ import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.manga.model.MangaUpdate
 import tachiyomi.domain.track.interactor.GetTracks
 import tachiyomi.domain.track.interactor.InsertTrack
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import java.util.Date

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt

@@ -6,8 +6,8 @@ import androidx.compose.runtime.getValue
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.MigrateSearchScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 
 class MigrateSearchScreen(private val mangaId: Long) : Screen() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt

@@ -21,9 +21,9 @@ import androidx.paging.compose.collectAsLazyPagingItems
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.BrowseSourceContent
 import eu.kanade.presentation.components.SearchToolbar
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.source.online.HttpSource

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt

@@ -8,11 +8,11 @@ import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.SourcesFilterScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.toast
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 class SourcesFilterScreen : Screen() {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt

@@ -36,7 +36,6 @@ import androidx.paging.compose.collectAsLazyPagingItems
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.BrowseSourceContent
 import eu.kanade.presentation.browse.MissingSourceScreen
 import eu.kanade.presentation.browse.components.BrowseSourceToolbar
@@ -44,6 +43,7 @@ import eu.kanade.presentation.browse.components.RemoveMangaDialog
 import eu.kanade.presentation.category.ChangeCategoryDialog
 import eu.kanade.presentation.manga.DuplicateMangaDialog
 import eu.kanade.presentation.util.AssistContentScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.CatalogueSource
 import eu.kanade.tachiyomi.source.LocalSource

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt

@@ -6,8 +6,8 @@ import androidx.compose.runtime.getValue
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.browse.GlobalSearchScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt

@@ -8,14 +8,14 @@ import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.category.CategoryScreen
 import eu.kanade.presentation.category.components.CategoryCreateDialog
 import eu.kanade.presentation.category.components.CategoryDeleteDialog
 import eu.kanade.presentation.category.components.CategoryRenameDialog
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.flow.collectLatest
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 class CategoryScreen : Screen() {
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt

@@ -47,10 +47,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.OverflowMenu
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.databinding.DownloadListBinding
@@ -58,6 +57,7 @@ import tachiyomi.core.util.lang.launchUI
 import tachiyomi.presentation.core.components.Pill
 import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
 import kotlin.math.roundToInt
 
 object DownloadQueueScreen : Screen() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt

@@ -34,9 +34,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
 import cafe.adriel.voyager.navigator.tab.TabNavigator
-import eu.kanade.core.navigation.Screen
 import eu.kanade.domain.library.service.LibraryPreferences
 import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.presentation.util.Screen
 import eu.kanade.presentation.util.isTabletUi
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.BrowseTab

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt

@@ -30,8 +30,6 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
 import cafe.adriel.voyager.navigator.tab.TabOptions
 import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.presentation.category.ChangeCategoryDialog
-import eu.kanade.presentation.components.EmptyScreen
-import eu.kanade.presentation.components.EmptyScreenAction
 import eu.kanade.presentation.library.DeleteLibraryMangaDialog
 import eu.kanade.presentation.library.LibrarySettingsDialog
 import eu.kanade.presentation.library.components.LibraryContent
@@ -55,8 +53,10 @@ import tachiyomi.domain.category.model.Category
 import tachiyomi.domain.library.model.LibraryManga
 import tachiyomi.domain.library.model.display
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.EmptyScreen
+import tachiyomi.presentation.core.screens.EmptyScreenAction
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 object LibraryTab : Tab {
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt

@@ -20,7 +20,6 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.domain.manga.model.hasCustomCover
 import eu.kanade.domain.manga.model.toSManga
 import eu.kanade.presentation.category.ChangeCategoryDialog
@@ -32,6 +31,7 @@ import eu.kanade.presentation.manga.MangaScreen
 import eu.kanade.presentation.manga.components.DeleteChaptersDialog
 import eu.kanade.presentation.manga.components.MangaCoverDialog
 import eu.kanade.presentation.util.AssistContentScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.presentation.util.isTabletUi
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.Source
@@ -54,7 +54,7 @@ import tachiyomi.core.util.lang.withIOContext
 import tachiyomi.core.util.system.logcat
 import tachiyomi.domain.chapter.model.Chapter
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 class MangaScreen(
     private val mangaId: Long,

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt

@@ -35,7 +35,6 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
 import eu.kanade.domain.track.model.toDbTrack
 import eu.kanade.domain.track.model.toDomainTrack
@@ -46,6 +45,7 @@ import eu.kanade.presentation.track.TrackInfoDialogHome
 import eu.kanade.presentation.track.TrackScoreSelector
 import eu.kanade.presentation.track.TrackServiceSearch
 import eu.kanade.presentation.track.TrackStatusSelector
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Track
 import eu.kanade.tachiyomi.data.track.EnhancedTrackService

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt

@@ -5,8 +5,8 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.more.NewUpdateScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.data.updater.AppUpdateService
 import eu.kanade.tachiyomi.util.system.openInBrowser
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt

@@ -12,13 +12,13 @@ import androidx.compose.ui.Modifier
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.more.settings.screen.AboutScreen
 import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
 import eu.kanade.presentation.more.settings.screen.SettingsGeneralScreen
 import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
 import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
 import eu.kanade.presentation.util.LocalBackPress
+import eu.kanade.presentation.util.Screen
 import eu.kanade.presentation.util.isTabletUi
 import tachiyomi.presentation.core.components.TwoPanelBox
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt

@@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.view.setComposeContent
-import tachiyomi.presentation.core.components.LoadingScreen
+import tachiyomi.presentation.core.screens.LoadingScreen
 import uy.kohesive.injekt.injectLazy
 
 abstract class BaseOAuthLoginActivity : BaseActivity() {

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt

@@ -7,13 +7,13 @@ import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.more.stats.StatsScreenContent
 import eu.kanade.presentation.more.stats.StatsScreenState
+import eu.kanade.presentation.util.Screen
 import eu.kanade.tachiyomi.R
-import tachiyomi.presentation.core.components.LoadingScreen
 import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.screens.LoadingScreen
 
 class StatsScreen : Screen() {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt

@@ -5,8 +5,8 @@ import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import eu.kanade.core.navigation.Screen
 import eu.kanade.presentation.util.AssistContentScreen
+import eu.kanade.presentation.util.Screen
 import eu.kanade.presentation.webview.WebViewScreenContent
 
 class WebViewScreen(

+ 1 - 0
presentation-core/build.gradle.kts

@@ -23,6 +23,7 @@ android {
 dependencies {
     // Compose
     implementation(platform(compose.bom))
+    implementation(compose.activity)
     implementation(compose.foundation)
     implementation(compose.material3.core)
     implementation(compose.material.core)

+ 261 - 0
presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt

@@ -0,0 +1,261 @@
+package tachiyomi.presentation.core.components
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.ZeroCornerSize
+import androidx.compose.material.SwipeableState
+import androidx.compose.material.rememberSwipeableState
+import androidx.compose.material.swipeable
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+import kotlin.time.Duration.Companion.milliseconds
+
+private const val SheetAnimationDuration = 500
+private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
+private const val ScrimAnimationDuration = 350
+private val ScrimAnimationSpec = tween<Float>(durationMillis = ScrimAnimationDuration)
+
+@Composable
+fun AdaptiveSheet(
+    isTabletUi: Boolean,
+    tonalElevation: Dp,
+    enableSwipeDismiss: Boolean,
+    onDismissRequest: () -> Unit,
+    content: @Composable () -> Unit,
+) {
+    val scope = rememberCoroutineScope()
+    if (isTabletUi) {
+        var targetAlpha by remember { mutableStateOf(0f) }
+        val alpha by animateFloatAsState(
+            targetValue = targetAlpha,
+            animationSpec = ScrimAnimationSpec,
+        )
+        val internalOnDismissRequest: () -> Unit = {
+            scope.launch {
+                targetAlpha = 0f
+                delay(ScrimAnimationSpec.durationMillis.milliseconds)
+                onDismissRequest()
+            }
+        }
+        BoxWithConstraints(
+            modifier = Modifier
+                .clickable(
+                    enabled = true,
+                    interactionSource = remember { MutableInteractionSource() },
+                    indication = null,
+                    onClick = internalOnDismissRequest,
+                )
+                .fillMaxSize()
+                .alpha(alpha),
+            contentAlignment = Alignment.Center,
+        ) {
+            Box(
+                modifier = Modifier
+                    .matchParentSize()
+                    .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
+            )
+            Surface(
+                modifier = Modifier
+                    .requiredWidthIn(max = 460.dp)
+                    .clickable(
+                        interactionSource = remember { MutableInteractionSource() },
+                        indication = null,
+                        onClick = {},
+                    )
+                    .systemBarsPadding()
+                    .padding(vertical = 16.dp),
+                shape = MaterialTheme.shapes.extraLarge,
+                tonalElevation = tonalElevation,
+                content = {
+                    BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
+                    content()
+                },
+            )
+
+            LaunchedEffect(Unit) {
+                targetAlpha = 1f
+            }
+        }
+    } else {
+        val swipeState = rememberSwipeableState(
+            initialValue = 1,
+            animationSpec = SheetAnimationSpec,
+        )
+        val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
+        BoxWithConstraints(
+            modifier = Modifier
+                .clickable(
+                    interactionSource = remember { MutableInteractionSource() },
+                    indication = null,
+                    onClick = internalOnDismissRequest,
+                )
+                .fillMaxSize(),
+            contentAlignment = Alignment.BottomCenter,
+        ) {
+            val fullHeight = constraints.maxHeight.toFloat()
+            val anchors = mapOf(0f to 0, fullHeight to 1)
+            val scrimAlpha by animateFloatAsState(
+                targetValue = if (swipeState.targetValue == 1) 0f else 1f,
+                animationSpec = ScrimAnimationSpec,
+            )
+            Box(
+                modifier = Modifier
+                    .matchParentSize()
+                    .alpha(scrimAlpha)
+                    .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
+            )
+            Surface(
+                modifier = Modifier
+                    .widthIn(max = 460.dp)
+                    .clickable(
+                        interactionSource = remember { MutableInteractionSource() },
+                        indication = null,
+                        onClick = {},
+                    )
+                    .nestedScroll(
+                        remember(enableSwipeDismiss, anchors) {
+                            swipeState.preUpPostDownNestedScrollConnection(
+                                enabled = enableSwipeDismiss,
+                                anchor = anchors,
+                            )
+                        },
+                    )
+                    .offset {
+                        IntOffset(
+                            0,
+                            swipeState.offset.value.roundToInt(),
+                        )
+                    }
+                    .swipeable(
+                        enabled = enableSwipeDismiss,
+                        state = swipeState,
+                        anchors = anchors,
+                        orientation = Orientation.Vertical,
+                        resistance = null,
+                    )
+                    .windowInsetsPadding(
+                        WindowInsets.systemBars
+                            .only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
+                    )
+                    .consumeWindowInsets(
+                        WindowInsets.systemBars
+                            .only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
+                    ),
+                shape = MaterialTheme.shapes.extraLarge.copy(bottomStart = ZeroCornerSize, bottomEnd = ZeroCornerSize),
+                tonalElevation = tonalElevation,
+                content = {
+                    BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
+                    content()
+                },
+            )
+
+            LaunchedEffect(swipeState) {
+                scope.launch { swipeState.animateTo(0) }
+                snapshotFlow { swipeState.currentValue }
+                    .drop(1)
+                    .filter { it == 1 }
+                    .collectLatest {
+                        delay(ScrimAnimationSpec.durationMillis.milliseconds)
+                        onDismissRequest()
+                    }
+            }
+        }
+    }
+}
+
+/**
+ * Yoinked from Swipeable.kt with modifications to disable
+ */
+private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
+    enabled: Boolean = true,
+    anchor: Map<Float, T>,
+) = object : NestedScrollConnection {
+    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+        val delta = available.toFloat()
+        return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
+            performDrag(delta).toOffset()
+        } else {
+            Offset.Zero
+        }
+    }
+
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource,
+    ): Offset {
+        return if (enabled && source == NestedScrollSource.Drag) {
+            performDrag(available.toFloat()).toOffset()
+        } else {
+            Offset.Zero
+        }
+    }
+
+    override suspend fun onPreFling(available: Velocity): Velocity {
+        val toFling = Offset(available.x, available.y).toFloat()
+        return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
+            performFling(velocity = toFling)
+            // since we go to the anchor with tween settling, consume all for the best UX
+            available
+        } else {
+            Velocity.Zero
+        }
+    }
+
+    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+        return if (enabled) {
+            performFling(velocity = Offset(available.x, available.y).toFloat())
+            available
+        } else {
+            Velocity.Zero
+        }
+    }
+
+    private fun Float.toOffset(): Offset = Offset(0f, this)
+
+    private fun Offset.toFloat(): Float = this.y
+}

+ 133 - 0
presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt

@@ -0,0 +1,133 @@
+package tachiyomi.presentation.core.screens
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.util.secondaryItemAlpha
+import kotlin.random.Random
+
+data class EmptyScreenAction(
+    @StringRes val stringResId: Int,
+    val icon: ImageVector,
+    val onClick: () -> Unit,
+)
+
+@Composable
+fun EmptyScreen(
+    @StringRes textResource: Int,
+    modifier: Modifier = Modifier,
+    actions: List<EmptyScreenAction>? = null,
+) {
+    EmptyScreen(
+        message = stringResource(textResource),
+        modifier = modifier,
+        actions = actions,
+    )
+}
+
+@Composable
+fun EmptyScreen(
+    message: String,
+    modifier: Modifier = Modifier,
+    actions: List<EmptyScreenAction>? = null,
+) {
+    val face = remember { getRandomErrorFace() }
+    Column(
+        modifier = modifier
+            .fillMaxSize()
+            .padding(horizontal = 24.dp),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center,
+    ) {
+        Text(
+            text = face,
+            modifier = Modifier.secondaryItemAlpha(),
+            style = MaterialTheme.typography.displayMedium,
+        )
+
+        Text(
+            text = message,
+            modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(),
+            style = MaterialTheme.typography.bodyMedium,
+            textAlign = TextAlign.Center,
+        )
+
+        if (!actions.isNullOrEmpty()) {
+            Row(
+                modifier = Modifier
+                    .padding(
+                        top = 24.dp,
+                        start = 24.dp,
+                        end = 24.dp,
+                    ),
+                horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
+            ) {
+                actions.forEach {
+                    ActionButton(
+                        modifier = Modifier.weight(1f),
+                        title = stringResource(it.stringResId),
+                        icon = it.icon,
+                        onClick = it.onClick,
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun ActionButton(
+    modifier: Modifier = Modifier,
+    title: String,
+    icon: ImageVector,
+    onClick: () -> Unit,
+) {
+    TextButton(
+        modifier = modifier,
+        onClick = onClick,
+    ) {
+        Column(horizontalAlignment = Alignment.CenterHorizontally) {
+            Icon(
+                imageVector = icon,
+                contentDescription = null,
+            )
+            Spacer(Modifier.height(4.dp))
+            Text(
+                text = title,
+                textAlign = TextAlign.Center,
+            )
+        }
+    }
+}
+
+private val ERROR_FACES = listOf(
+    "(・o・;)",
+    "Σ(ಠ_ಠ)",
+    "ಥ_ಥ",
+    "(˘・_・˘)",
+    "(; ̄Д ̄)",
+    "(・Д・。",
+)
+
+private fun getRandomErrorFace(): String {
+    return ERROR_FACES[Random.nextInt(ERROR_FACES.size)]
+}

+ 12 - 15
app/src/main/java/eu/kanade/presentation/components/InfoScaffold.kt → presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.components
+package tachiyomi.presentation.core.screens
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
@@ -26,14 +26,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
-import eu.kanade.presentation.theme.TachiyomiTheme
 import tachiyomi.presentation.core.components.material.Scaffold
 import tachiyomi.presentation.core.components.material.padding
 import tachiyomi.presentation.core.util.ThemePreviews
 import tachiyomi.presentation.core.util.secondaryItemAlpha
 
 @Composable
-fun InfoScaffold(
+fun InfoScreen(
     icon: ImageVector,
     headingText: String,
     subtitleText: String,
@@ -125,17 +124,15 @@ fun InfoScaffold(
 @ThemePreviews
 @Composable
 private fun InfoScaffoldPreview() {
-    TachiyomiTheme {
-        InfoScaffold(
-            icon = Icons.Outlined.Newspaper,
-            headingText = "Heading",
-            subtitleText = "Subtitle",
-            acceptText = "Accept",
-            onAcceptClick = {},
-            rejectText = "Reject",
-            onRejectClick = {},
-        ) {
-            Text("Hello world")
-        }
+    InfoScreen(
+        icon = Icons.Outlined.Newspaper,
+        headingText = "Heading",
+        subtitleText = "Subtitle",
+        acceptText = "Accept",
+        onAcceptClick = {},
+        rejectText = "Reject",
+        onRejectClick = {},
+    ) {
+        Text("Hello world")
     }
 }

+ 1 - 1
presentation-core/src/main/java/tachiyomi/presentation/core/components/LoadingScreen.kt → presentation-core/src/main/java/tachiyomi/presentation/core/screens/LoadingScreen.kt

@@ -1,4 +1,4 @@
-package tachiyomi.presentation.core.components
+package tachiyomi.presentation.core.screens
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize