mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Move more components to presentation-core module
This commit is contained in:
		@@ -48,7 +48,6 @@ 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.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.WarningBanner
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
 | 
			
		||||
@@ -57,6 +56,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.source.ConfigurableSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.DIVIDER_ALPHA
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,11 @@ 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.components.FastScrollLazyColumn
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -38,21 +38,21 @@ 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.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
 | 
			
		||||
import eu.kanade.presentation.theme.header
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.InstallStep
 | 
			
		||||
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.util.plus
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ExtensionScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,13 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
 | 
			
		||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
 | 
			
		||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
 | 
			
		||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,11 @@ 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.components.FastScrollLazyColumn
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,12 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
 | 
			
		||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
 | 
			
		||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
 | 
			
		||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,7 @@ 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.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
 | 
			
		||||
import eu.kanade.presentation.theme.header
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
 | 
			
		||||
@@ -38,8 +34,12 @@ 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.util.plus
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MigrateSourceScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,12 @@ 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.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -23,9 +23,7 @@ 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.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.theme.header
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
 | 
			
		||||
@@ -34,8 +32,10 @@ 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.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SourcesScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,10 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
import tachiyomi.domain.source.model.Source
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BaseSourceItem(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,12 @@ import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import eu.kanade.presentation.browse.InLibraryBadge
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.MangaComfortableGridItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaComfortableGridItem
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
import tachiyomi.presentation.core.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceComfortableGrid(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,12 @@ import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import eu.kanade.presentation.browse.InLibraryBadge
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.MangaCompactGridItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaCompactGridItem
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
import tachiyomi.presentation.core.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceCompactGrid(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,13 @@ import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import androidx.paging.compose.items
 | 
			
		||||
import eu.kanade.presentation.browse.InLibraryBadge
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaListItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaListItem
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceList(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.browse.InLibraryBadge
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.MangaComfortableGridItem
 | 
			
		||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaComfortableGridItem
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,13 @@ 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.presentation.util.plus
 | 
			
		||||
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.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CategoryScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import tachiyomi.domain.category.model.Category
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,10 @@ import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToEnd
 | 
			
		||||
import eu.kanade.presentation.util.isScrollingUp
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrollingUp
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CategoryFloatingActionButton(
 | 
			
		||||
 
 | 
			
		||||
@@ -44,9 +44,9 @@ import androidx.compose.ui.text.input.VisualTransformation
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.unit.sp
 | 
			
		||||
import eu.kanade.presentation.util.runOnEnterKeyPressed
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
const val SEARCH_DEBOUNCE_MILLIS = 250L
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,386 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.core.Animatable
 | 
			
		||||
import androidx.compose.animation.core.VectorConverter
 | 
			
		||||
import androidx.compose.foundation.BorderStroke
 | 
			
		||||
import androidx.compose.foundation.interaction.FocusInteraction
 | 
			
		||||
import androidx.compose.foundation.interaction.HoverInteraction
 | 
			
		||||
import androidx.compose.foundation.interaction.Interaction
 | 
			
		||||
import androidx.compose.foundation.interaction.InteractionSource
 | 
			
		||||
import androidx.compose.foundation.interaction.MutableInteractionSource
 | 
			
		||||
import androidx.compose.foundation.interaction.PressInteraction
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.RowScope
 | 
			
		||||
import androidx.compose.foundation.layout.defaultMinSize
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material3.Button
 | 
			
		||||
import androidx.compose.material3.ColorScheme
 | 
			
		||||
import androidx.compose.material3.ElevatedButton
 | 
			
		||||
import androidx.compose.material3.LocalContentColor
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.ProvideTextStyle
 | 
			
		||||
import androidx.compose.material3.TextButton
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.CompositionLocalProvider
 | 
			
		||||
import androidx.compose.runtime.Immutable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateListOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.rememberUpdatedState
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.geometry.Offset
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.Shape
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.util.animateElevation
 | 
			
		||||
import androidx.compose.material3.ButtonDefaults as M3ButtonDefaults
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TextButton with additional onLongClick functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @see androidx.compose.material3.TextButton
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun TextButton(
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    onLongClick: (() -> Unit)? = null,
 | 
			
		||||
    enabled: Boolean = true,
 | 
			
		||||
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
 | 
			
		||||
    elevation: ButtonElevation? = null,
 | 
			
		||||
    shape: Shape = M3ButtonDefaults.textShape,
 | 
			
		||||
    border: BorderStroke? = null,
 | 
			
		||||
    colors: ButtonColors = ButtonColors(
 | 
			
		||||
        containerColor = Color.Transparent,
 | 
			
		||||
        contentColor = MaterialTheme.colorScheme.primary,
 | 
			
		||||
        disabledContainerColor = Color.Transparent,
 | 
			
		||||
        disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
 | 
			
		||||
    ),
 | 
			
		||||
    contentPadding: PaddingValues = M3ButtonDefaults.TextButtonContentPadding,
 | 
			
		||||
    content: @Composable RowScope.() -> Unit,
 | 
			
		||||
) =
 | 
			
		||||
    Button(
 | 
			
		||||
        onClick = onClick,
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        onLongClick = onLongClick,
 | 
			
		||||
        enabled = enabled,
 | 
			
		||||
        interactionSource = interactionSource,
 | 
			
		||||
        elevation = elevation,
 | 
			
		||||
        shape = shape,
 | 
			
		||||
        border = border,
 | 
			
		||||
        colors = colors,
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
        content = content,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Button with additional onLongClick functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @see androidx.compose.material3.TextButton
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun Button(
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    onLongClick: (() -> Unit)? = null,
 | 
			
		||||
    enabled: Boolean = true,
 | 
			
		||||
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
 | 
			
		||||
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
 | 
			
		||||
    shape: Shape = M3ButtonDefaults.textShape,
 | 
			
		||||
    border: BorderStroke? = null,
 | 
			
		||||
    colors: ButtonColors = ButtonDefaults.buttonColors(),
 | 
			
		||||
    contentPadding: PaddingValues = M3ButtonDefaults.ContentPadding,
 | 
			
		||||
    content: @Composable RowScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val containerColor = colors.containerColor(enabled).value
 | 
			
		||||
    val contentColor = colors.contentColor(enabled).value
 | 
			
		||||
    val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
 | 
			
		||||
    val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp
 | 
			
		||||
 | 
			
		||||
    Surface(
 | 
			
		||||
        onClick = onClick,
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        onLongClick = onLongClick,
 | 
			
		||||
        shape = shape,
 | 
			
		||||
        color = containerColor,
 | 
			
		||||
        contentColor = contentColor,
 | 
			
		||||
        tonalElevation = tonalElevation,
 | 
			
		||||
        shadowElevation = shadowElevation,
 | 
			
		||||
        border = border,
 | 
			
		||||
        interactionSource = interactionSource,
 | 
			
		||||
        enabled = enabled,
 | 
			
		||||
    ) {
 | 
			
		||||
        CompositionLocalProvider(LocalContentColor provides contentColor) {
 | 
			
		||||
            ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
 | 
			
		||||
                Row(
 | 
			
		||||
                    Modifier.defaultMinSize(
 | 
			
		||||
                        minWidth = M3ButtonDefaults.MinWidth,
 | 
			
		||||
                        minHeight = M3ButtonDefaults.MinHeight,
 | 
			
		||||
                    )
 | 
			
		||||
                        .padding(contentPadding),
 | 
			
		||||
                    horizontalArrangement = Arrangement.Center,
 | 
			
		||||
                    verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                    content = content,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object ButtonDefaults {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a [ButtonColors] that represents the default container and content colors used in a
 | 
			
		||||
     * [Button].
 | 
			
		||||
     *
 | 
			
		||||
     * @param containerColor the container color of this [Button] when enabled.
 | 
			
		||||
     * @param contentColor the content color of this [Button] when enabled.
 | 
			
		||||
     * @param disabledContainerColor the container color of this [Button] when not enabled.
 | 
			
		||||
     * @param disabledContentColor the content color of this [Button] when not enabled.
 | 
			
		||||
     */
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun buttonColors(
 | 
			
		||||
        containerColor: Color = MaterialTheme.colorScheme.primary,
 | 
			
		||||
        contentColor: Color = MaterialTheme.colorScheme.onPrimary,
 | 
			
		||||
        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
 | 
			
		||||
        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
 | 
			
		||||
    ): ButtonColors = ButtonColors(
 | 
			
		||||
        containerColor = containerColor,
 | 
			
		||||
        contentColor = contentColor,
 | 
			
		||||
        disabledContainerColor = disabledContainerColor,
 | 
			
		||||
        disabledContentColor = disabledContentColor,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a [ButtonElevation] that will animate between the provided values according to the
 | 
			
		||||
     * Material specification for a [Button].
 | 
			
		||||
     *
 | 
			
		||||
     * @param defaultElevation the elevation used when the [Button] is enabled, and has no other
 | 
			
		||||
     * [Interaction]s.
 | 
			
		||||
     * @param pressedElevation the elevation used when this [Button] is enabled and pressed.
 | 
			
		||||
     * @param focusedElevation the elevation used when the [Button] is enabled and focused.
 | 
			
		||||
     * @param hoveredElevation the elevation used when the [Button] is enabled and hovered.
 | 
			
		||||
     * @param disabledElevation the elevation used when the [Button] is not enabled.
 | 
			
		||||
     */
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun buttonElevation(
 | 
			
		||||
        defaultElevation: Dp = 0.dp,
 | 
			
		||||
        pressedElevation: Dp = 0.dp,
 | 
			
		||||
        focusedElevation: Dp = 0.dp,
 | 
			
		||||
        hoveredElevation: Dp = 1.dp,
 | 
			
		||||
        disabledElevation: Dp = 0.dp,
 | 
			
		||||
    ): ButtonElevation = ButtonElevation(
 | 
			
		||||
        defaultElevation = defaultElevation,
 | 
			
		||||
        pressedElevation = pressedElevation,
 | 
			
		||||
        focusedElevation = focusedElevation,
 | 
			
		||||
        hoveredElevation = hoveredElevation,
 | 
			
		||||
        disabledElevation = disabledElevation,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents the elevation for a button in different states.
 | 
			
		||||
 *
 | 
			
		||||
 * - See [M3ButtonDefaults.buttonElevation] for the default elevation used in a [Button].
 | 
			
		||||
 * - See [M3ButtonDefaults.elevatedButtonElevation] for the default elevation used in a
 | 
			
		||||
 * [ElevatedButton].
 | 
			
		||||
 */
 | 
			
		||||
@Stable
 | 
			
		||||
class ButtonElevation internal constructor(
 | 
			
		||||
    private val defaultElevation: Dp,
 | 
			
		||||
    private val pressedElevation: Dp,
 | 
			
		||||
    private val focusedElevation: Dp,
 | 
			
		||||
    private val hoveredElevation: Dp,
 | 
			
		||||
    private val disabledElevation: Dp,
 | 
			
		||||
) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Represents the tonal elevation used in a button, depending on its [enabled] state and
 | 
			
		||||
     * [interactionSource]. This should typically be the same value as the [shadowElevation].
 | 
			
		||||
     *
 | 
			
		||||
     * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
 | 
			
		||||
     * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
 | 
			
		||||
     * color in light theme and lighter color in dark theme.
 | 
			
		||||
     *
 | 
			
		||||
     * See [shadowElevation] which controls the elevation of the shadow drawn around the button.
 | 
			
		||||
     *
 | 
			
		||||
     * @param enabled whether the button is enabled
 | 
			
		||||
     * @param interactionSource the [InteractionSource] for this button
 | 
			
		||||
     */
 | 
			
		||||
    @Composable
 | 
			
		||||
    internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
 | 
			
		||||
        return animateElevation(enabled = enabled, interactionSource = interactionSource)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Represents the shadow elevation used in a button, depending on its [enabled] state and
 | 
			
		||||
     * [interactionSource]. This should typically be the same value as the [tonalElevation].
 | 
			
		||||
     *
 | 
			
		||||
     * Shadow elevation is used to apply a shadow around the button to give it higher emphasis.
 | 
			
		||||
     *
 | 
			
		||||
     * See [tonalElevation] which controls the elevation with a color shift to the surface.
 | 
			
		||||
     *
 | 
			
		||||
     * @param enabled whether the button is enabled
 | 
			
		||||
     * @param interactionSource the [InteractionSource] for this button
 | 
			
		||||
     */
 | 
			
		||||
    @Composable
 | 
			
		||||
    internal fun shadowElevation(
 | 
			
		||||
        enabled: Boolean,
 | 
			
		||||
        interactionSource: InteractionSource,
 | 
			
		||||
    ): State<Dp> {
 | 
			
		||||
        return animateElevation(enabled = enabled, interactionSource = interactionSource)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    private fun animateElevation(
 | 
			
		||||
        enabled: Boolean,
 | 
			
		||||
        interactionSource: InteractionSource,
 | 
			
		||||
    ): State<Dp> {
 | 
			
		||||
        val interactions = remember { mutableStateListOf<Interaction>() }
 | 
			
		||||
        LaunchedEffect(interactionSource) {
 | 
			
		||||
            interactionSource.interactions.collect { interaction ->
 | 
			
		||||
                when (interaction) {
 | 
			
		||||
                    is HoverInteraction.Enter -> {
 | 
			
		||||
                        interactions.add(interaction)
 | 
			
		||||
                    }
 | 
			
		||||
                    is HoverInteraction.Exit -> {
 | 
			
		||||
                        interactions.remove(interaction.enter)
 | 
			
		||||
                    }
 | 
			
		||||
                    is FocusInteraction.Focus -> {
 | 
			
		||||
                        interactions.add(interaction)
 | 
			
		||||
                    }
 | 
			
		||||
                    is FocusInteraction.Unfocus -> {
 | 
			
		||||
                        interactions.remove(interaction.focus)
 | 
			
		||||
                    }
 | 
			
		||||
                    is PressInteraction.Press -> {
 | 
			
		||||
                        interactions.add(interaction)
 | 
			
		||||
                    }
 | 
			
		||||
                    is PressInteraction.Release -> {
 | 
			
		||||
                        interactions.remove(interaction.press)
 | 
			
		||||
                    }
 | 
			
		||||
                    is PressInteraction.Cancel -> {
 | 
			
		||||
                        interactions.remove(interaction.press)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val interaction = interactions.lastOrNull()
 | 
			
		||||
 | 
			
		||||
        val target =
 | 
			
		||||
            if (!enabled) {
 | 
			
		||||
                disabledElevation
 | 
			
		||||
            } else {
 | 
			
		||||
                when (interaction) {
 | 
			
		||||
                    is PressInteraction.Press -> pressedElevation
 | 
			
		||||
                    is HoverInteraction.Enter -> hoveredElevation
 | 
			
		||||
                    is FocusInteraction.Focus -> focusedElevation
 | 
			
		||||
                    else -> defaultElevation
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        val animatable = remember { Animatable(target, Dp.VectorConverter) }
 | 
			
		||||
 | 
			
		||||
        if (!enabled) {
 | 
			
		||||
            // No transition when moving to a disabled state
 | 
			
		||||
            LaunchedEffect(target) { animatable.snapTo(target) }
 | 
			
		||||
        } else {
 | 
			
		||||
            LaunchedEffect(target) {
 | 
			
		||||
                val lastInteraction = when (animatable.targetValue) {
 | 
			
		||||
                    pressedElevation -> PressInteraction.Press(Offset.Zero)
 | 
			
		||||
                    hoveredElevation -> HoverInteraction.Enter()
 | 
			
		||||
                    focusedElevation -> FocusInteraction.Focus()
 | 
			
		||||
                    else -> null
 | 
			
		||||
                }
 | 
			
		||||
                animatable.animateElevation(
 | 
			
		||||
                    from = lastInteraction,
 | 
			
		||||
                    to = interaction,
 | 
			
		||||
                    target = target,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return animatable.asState()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other == null || other !is ButtonElevation) return false
 | 
			
		||||
 | 
			
		||||
        if (defaultElevation != other.defaultElevation) return false
 | 
			
		||||
        if (pressedElevation != other.pressedElevation) return false
 | 
			
		||||
        if (focusedElevation != other.focusedElevation) return false
 | 
			
		||||
        if (hoveredElevation != other.hoveredElevation) return false
 | 
			
		||||
        if (disabledElevation != other.disabledElevation) return false
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        var result = defaultElevation.hashCode()
 | 
			
		||||
        result = 31 * result + pressedElevation.hashCode()
 | 
			
		||||
        result = 31 * result + focusedElevation.hashCode()
 | 
			
		||||
        result = 31 * result + hoveredElevation.hashCode()
 | 
			
		||||
        result = 31 * result + disabledElevation.hashCode()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents the container and content colors used in a button in different states.
 | 
			
		||||
 *
 | 
			
		||||
 * - See [M3ButtonDefaults.buttonColors] for the default colors used in a [Button].
 | 
			
		||||
 * - See [M3ButtonDefaults.elevatedButtonColors] for the default colors used in a [ElevatedButton].
 | 
			
		||||
 * - See [M3ButtonDefaults.textButtonColors] for the default colors used in a [TextButton].
 | 
			
		||||
 */
 | 
			
		||||
@Immutable
 | 
			
		||||
class ButtonColors internal constructor(
 | 
			
		||||
    private val containerColor: Color,
 | 
			
		||||
    private val contentColor: Color,
 | 
			
		||||
    private val disabledContainerColor: Color,
 | 
			
		||||
    private val disabledContentColor: Color,
 | 
			
		||||
) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Represents the container color for this button, depending on [enabled].
 | 
			
		||||
     *
 | 
			
		||||
     * @param enabled whether the button is enabled
 | 
			
		||||
     */
 | 
			
		||||
    @Composable
 | 
			
		||||
    internal fun containerColor(enabled: Boolean): State<Color> {
 | 
			
		||||
        return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Represents the content color for this button, depending on [enabled].
 | 
			
		||||
     *
 | 
			
		||||
     * @param enabled whether the button is enabled
 | 
			
		||||
     */
 | 
			
		||||
    @Composable
 | 
			
		||||
    internal fun contentColor(enabled: Boolean): State<Color> {
 | 
			
		||||
        return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other == null || other !is ButtonColors) return false
 | 
			
		||||
 | 
			
		||||
        if (containerColor != other.containerColor) return false
 | 
			
		||||
        if (contentColor != other.contentColor) return false
 | 
			
		||||
        if (disabledContainerColor != other.disabledContainerColor) return false
 | 
			
		||||
        if (disabledContentColor != other.disabledContentColor) return false
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        var result = containerColor.hashCode()
 | 
			
		||||
        result = 31 * result + contentColor.hashCode()
 | 
			
		||||
        result = 31 * result + disabledContainerColor.hashCode()
 | 
			
		||||
        result = 31 * result + disabledContentColor.hashCode()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -26,10 +26,10 @@ 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.presentation.util.ThemePreviews
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
import kotlin.random.Random
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -27,10 +27,10 @@ import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.zIndex
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.presentation.util.ThemePreviews
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
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(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.gestures.FlingBehavior
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyGridState
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.util.flingBehaviorIgnoringMotionScale
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun FastScrollLazyVerticalGrid(
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    state: LazyGridState = rememberLazyGridState(),
 | 
			
		||||
    thumbAllowed: () -> Boolean = { true },
 | 
			
		||||
    thumbColor: Color = MaterialTheme.colorScheme.primary,
 | 
			
		||||
    contentPadding: PaddingValues = PaddingValues(0.dp),
 | 
			
		||||
    topContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    bottomContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    endContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    reverseLayout: Boolean = false,
 | 
			
		||||
    verticalArrangement: Arrangement.Vertical =
 | 
			
		||||
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
 | 
			
		||||
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
 | 
			
		||||
    flingBehavior: FlingBehavior = flingBehaviorIgnoringMotionScale(),
 | 
			
		||||
    userScrollEnabled: Boolean = true,
 | 
			
		||||
    content: LazyGridScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    VerticalGridFastScroller(
 | 
			
		||||
        state = state,
 | 
			
		||||
        columns = columns,
 | 
			
		||||
        arrangement = horizontalArrangement,
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        thumbAllowed = thumbAllowed,
 | 
			
		||||
        thumbColor = thumbColor,
 | 
			
		||||
        topContentPadding = topContentPadding,
 | 
			
		||||
        bottomContentPadding = bottomContentPadding,
 | 
			
		||||
        endContentPadding = endContentPadding,
 | 
			
		||||
    ) {
 | 
			
		||||
        LazyVerticalGrid(
 | 
			
		||||
            columns = columns,
 | 
			
		||||
            state = state,
 | 
			
		||||
            contentPadding = contentPadding,
 | 
			
		||||
            reverseLayout = reverseLayout,
 | 
			
		||||
            verticalArrangement = verticalArrangement,
 | 
			
		||||
            horizontalArrangement = horizontalArrangement,
 | 
			
		||||
            flingBehavior = flingBehavior,
 | 
			
		||||
            userScrollEnabled = userScrollEnabled,
 | 
			
		||||
            content = content,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,123 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.gestures.FlingBehavior
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.calculateEndPadding
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListScope
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.foundation.lazy.rememberLazyListState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalDensity
 | 
			
		||||
import androidx.compose.ui.platform.LocalLayoutDirection
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.util.drawVerticalScrollbar
 | 
			
		||||
import eu.kanade.presentation.util.flingBehaviorIgnoringMotionScale
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LazyColumn with fling animation fix
 | 
			
		||||
 *
 | 
			
		||||
 * @see flingBehaviorIgnoringMotionScale
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyColumn(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    state: LazyListState = rememberLazyListState(),
 | 
			
		||||
    contentPadding: PaddingValues = PaddingValues(0.dp),
 | 
			
		||||
    reverseLayout: Boolean = false,
 | 
			
		||||
    verticalArrangement: Arrangement.Vertical =
 | 
			
		||||
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
 | 
			
		||||
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
 | 
			
		||||
    flingBehavior: FlingBehavior = flingBehaviorIgnoringMotionScale(),
 | 
			
		||||
    userScrollEnabled: Boolean = true,
 | 
			
		||||
    content: LazyListScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    androidx.compose.foundation.lazy.LazyColumn(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        state = state,
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
        reverseLayout = reverseLayout,
 | 
			
		||||
        verticalArrangement = verticalArrangement,
 | 
			
		||||
        horizontalAlignment = horizontalAlignment,
 | 
			
		||||
        flingBehavior = flingBehavior,
 | 
			
		||||
        userScrollEnabled = userScrollEnabled,
 | 
			
		||||
        content = content,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LazyColumn with scrollbar.
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun ScrollbarLazyColumn(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    state: LazyListState = rememberLazyListState(),
 | 
			
		||||
    contentPadding: PaddingValues = PaddingValues(0.dp),
 | 
			
		||||
    reverseLayout: Boolean = false,
 | 
			
		||||
    verticalArrangement: Arrangement.Vertical =
 | 
			
		||||
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
 | 
			
		||||
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
 | 
			
		||||
    flingBehavior: FlingBehavior = flingBehaviorIgnoringMotionScale(),
 | 
			
		||||
    userScrollEnabled: Boolean = true,
 | 
			
		||||
    content: LazyListScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val direction = LocalLayoutDirection.current
 | 
			
		||||
    val density = LocalDensity.current
 | 
			
		||||
    val positionOffset = remember(contentPadding) {
 | 
			
		||||
        with(density) { contentPadding.calculateEndPadding(direction).toPx() }
 | 
			
		||||
    }
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .drawVerticalScrollbar(
 | 
			
		||||
                state = state,
 | 
			
		||||
                reverseScrolling = reverseLayout,
 | 
			
		||||
                positionOffsetPx = positionOffset,
 | 
			
		||||
            ),
 | 
			
		||||
        state = state,
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
        reverseLayout = reverseLayout,
 | 
			
		||||
        verticalArrangement = verticalArrangement,
 | 
			
		||||
        horizontalAlignment = horizontalAlignment,
 | 
			
		||||
        flingBehavior = flingBehavior,
 | 
			
		||||
        userScrollEnabled = userScrollEnabled,
 | 
			
		||||
        content = content,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LazyColumn with fast scroller.
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun FastScrollLazyColumn(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    state: LazyListState = rememberLazyListState(),
 | 
			
		||||
    contentPadding: PaddingValues = PaddingValues(0.dp),
 | 
			
		||||
    reverseLayout: Boolean = false,
 | 
			
		||||
    verticalArrangement: Arrangement.Vertical =
 | 
			
		||||
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
 | 
			
		||||
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
 | 
			
		||||
    flingBehavior: FlingBehavior = flingBehaviorIgnoringMotionScale(),
 | 
			
		||||
    userScrollEnabled: Boolean = true,
 | 
			
		||||
    content: LazyListScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    VerticalFastScroller(
 | 
			
		||||
        listState = state,
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        topContentPadding = contentPadding.calculateTopPadding(),
 | 
			
		||||
        endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
 | 
			
		||||
    ) {
 | 
			
		||||
        LazyColumn(
 | 
			
		||||
            state = state,
 | 
			
		||||
            contentPadding = contentPadding,
 | 
			
		||||
            reverseLayout = reverseLayout,
 | 
			
		||||
            verticalArrangement = verticalArrangement,
 | 
			
		||||
            horizontalAlignment = horizontalAlignment,
 | 
			
		||||
            flingBehavior = flingBehavior,
 | 
			
		||||
            userScrollEnabled = userScrollEnabled,
 | 
			
		||||
            content = content,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ListGroupHeader(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    text: String,
 | 
			
		||||
) {
 | 
			
		||||
    Text(
 | 
			
		||||
        text = text,
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .padding(
 | 
			
		||||
                horizontal = MaterialTheme.padding.medium,
 | 
			
		||||
                vertical = MaterialTheme.padding.small,
 | 
			
		||||
            ),
 | 
			
		||||
        color = MaterialTheme.colorScheme.onSurfaceVariant,
 | 
			
		||||
        fontWeight = FontWeight.SemiBold,
 | 
			
		||||
        style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -31,6 +31,7 @@ import androidx.compose.ui.util.fastForEach
 | 
			
		||||
import androidx.compose.ui.util.fastMaxBy
 | 
			
		||||
import androidx.compose.ui.util.fastSumBy
 | 
			
		||||
import kotlinx.coroutines.flow.distinctUntilChanged
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun HorizontalPager(
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
 | 
			
		||||
import tachiyomi.presentation.core.components.ListGroupHeader
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,113 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.BorderStroke
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.border
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.interaction.MutableInteractionSource
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.material.ripple.rememberRipple
 | 
			
		||||
import androidx.compose.material3.ColorScheme
 | 
			
		||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
 | 
			
		||||
import androidx.compose.material3.LocalContentColor
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.contentColorFor
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.CompositionLocalProvider
 | 
			
		||||
import androidx.compose.runtime.NonRestartableComposable
 | 
			
		||||
import androidx.compose.runtime.ReadOnlyComposable
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.draw.shadow
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.RectangleShape
 | 
			
		||||
import androidx.compose.ui.graphics.Shape
 | 
			
		||||
import androidx.compose.ui.graphics.compositeOver
 | 
			
		||||
import androidx.compose.ui.semantics.Role
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.util.minimumTouchTargetSize
 | 
			
		||||
import kotlin.math.ln
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Surface with additional onLongClick functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @see androidx.compose.material3.Surface
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
@NonRestartableComposable
 | 
			
		||||
fun Surface(
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    onLongClick: (() -> Unit)? = null,
 | 
			
		||||
    enabled: Boolean = true,
 | 
			
		||||
    shape: Shape = RectangleShape,
 | 
			
		||||
    color: Color = MaterialTheme.colorScheme.surface,
 | 
			
		||||
    contentColor: Color = contentColorFor(color),
 | 
			
		||||
    tonalElevation: Dp = 0.dp,
 | 
			
		||||
    shadowElevation: Dp = 0.dp,
 | 
			
		||||
    border: BorderStroke? = null,
 | 
			
		||||
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
 | 
			
		||||
    content: @Composable () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
 | 
			
		||||
    CompositionLocalProvider(
 | 
			
		||||
        LocalContentColor provides contentColor,
 | 
			
		||||
        LocalAbsoluteTonalElevation provides absoluteElevation,
 | 
			
		||||
    ) {
 | 
			
		||||
        Box(
 | 
			
		||||
            modifier = modifier
 | 
			
		||||
                .minimumTouchTargetSize()
 | 
			
		||||
                .surface(
 | 
			
		||||
                    shape = shape,
 | 
			
		||||
                    backgroundColor = surfaceColorAtElevation(
 | 
			
		||||
                        color = color,
 | 
			
		||||
                        elevation = absoluteElevation,
 | 
			
		||||
                    ),
 | 
			
		||||
                    border = border,
 | 
			
		||||
                    shadowElevation = shadowElevation,
 | 
			
		||||
                )
 | 
			
		||||
                .combinedClickable(
 | 
			
		||||
                    interactionSource = interactionSource,
 | 
			
		||||
                    indication = rememberRipple(),
 | 
			
		||||
                    enabled = enabled,
 | 
			
		||||
                    role = Role.Button,
 | 
			
		||||
                    onLongClick = onLongClick,
 | 
			
		||||
                    onClick = onClick,
 | 
			
		||||
                ),
 | 
			
		||||
            propagateMinConstraints = true,
 | 
			
		||||
        ) {
 | 
			
		||||
            content()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun Modifier.surface(
 | 
			
		||||
    shape: Shape,
 | 
			
		||||
    backgroundColor: Color,
 | 
			
		||||
    border: BorderStroke?,
 | 
			
		||||
    shadowElevation: Dp,
 | 
			
		||||
) = this
 | 
			
		||||
    .shadow(shadowElevation, shape, clip = false)
 | 
			
		||||
    .then(if (border != null) Modifier.border(border, shape) else Modifier)
 | 
			
		||||
    .background(color = backgroundColor, shape = shape)
 | 
			
		||||
    .clip(shape)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@ReadOnlyComposable
 | 
			
		||||
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
 | 
			
		||||
    return if (color == MaterialTheme.colorScheme.surface) {
 | 
			
		||||
        MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
 | 
			
		||||
    } else {
 | 
			
		||||
        color
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun ColorScheme.surfaceColorAtElevation(
 | 
			
		||||
    elevation: Dp,
 | 
			
		||||
): Color {
 | 
			
		||||
    if (elevation == 0.dp) return surface
 | 
			
		||||
    val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
 | 
			
		||||
    return surfaceTint.copy(alpha = alpha).compositeOver(surface)
 | 
			
		||||
}
 | 
			
		||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.util.fastForEachIndexed
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TabIndicator
 | 
			
		||||
 | 
			
		||||
object TabbedDialogPaddings {
 | 
			
		||||
    val Horizontal = 24.dp
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TabIndicator
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TabText
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TabbedScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.isSystemInDarkTheme
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.TabPosition
 | 
			
		||||
import androidx.compose.material3.TabRowDefaults
 | 
			
		||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.unit.sp
 | 
			
		||||
import tachiyomi.presentation.core.components.Pill
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TabIndicator(currentTabPosition: TabPosition) {
 | 
			
		||||
    TabRowDefaults.Indicator(
 | 
			
		||||
        Modifier
 | 
			
		||||
            .tabIndicatorOffset(currentTabPosition)
 | 
			
		||||
            .padding(horizontal = 8.dp)
 | 
			
		||||
            .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TabText(
 | 
			
		||||
    text: String,
 | 
			
		||||
    badgeCount: Int? = null,
 | 
			
		||||
) {
 | 
			
		||||
    val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
 | 
			
		||||
 | 
			
		||||
    Row(
 | 
			
		||||
        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
    ) {
 | 
			
		||||
        Text(text = text)
 | 
			
		||||
        if (badgeCount != null) {
 | 
			
		||||
            Pill(
 | 
			
		||||
                text = "$badgeCount",
 | 
			
		||||
                color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
 | 
			
		||||
                fontSize = 10.sp,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,438 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import android.view.ViewConfiguration
 | 
			
		||||
import androidx.compose.animation.core.Animatable
 | 
			
		||||
import androidx.compose.animation.core.tween
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.gestures.Orientation
 | 
			
		||||
import androidx.compose.foundation.gestures.draggable
 | 
			
		||||
import androidx.compose.foundation.gestures.rememberDraggableState
 | 
			
		||||
import androidx.compose.foundation.interaction.MutableInteractionSource
 | 
			
		||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.calculateEndPadding
 | 
			
		||||
import androidx.compose.foundation.layout.calculateStartPadding
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
import androidx.compose.foundation.layout.offset
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListItemInfo
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyGridState
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.foundation.systemGestureExclusion
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
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.setValue
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.alpha
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.layout.SubcomposeLayout
 | 
			
		||||
import androidx.compose.ui.platform.LocalDensity
 | 
			
		||||
import androidx.compose.ui.unit.Constraints
 | 
			
		||||
import androidx.compose.ui.unit.Density
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.IntOffset
 | 
			
		||||
import androidx.compose.ui.unit.LayoutDirection
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.util.fastFirstOrNull
 | 
			
		||||
import androidx.compose.ui.util.fastForEach
 | 
			
		||||
import androidx.compose.ui.util.fastMaxBy
 | 
			
		||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
 | 
			
		||||
import kotlinx.coroutines.channels.BufferOverflow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws vertical fast scroller to a lazy list
 | 
			
		||||
 *
 | 
			
		||||
 * Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun VerticalFastScroller(
 | 
			
		||||
    listState: LazyListState,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    thumbAllowed: () -> Boolean = { true },
 | 
			
		||||
    thumbColor: Color = MaterialTheme.colorScheme.primary,
 | 
			
		||||
    topContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    bottomContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    endContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    content: @Composable () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    SubcomposeLayout(modifier = modifier) { constraints ->
 | 
			
		||||
        val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
 | 
			
		||||
        val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
 | 
			
		||||
        val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
 | 
			
		||||
 | 
			
		||||
        val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
 | 
			
		||||
        val scrollerPlaceable = subcompose("scroller") {
 | 
			
		||||
            val layoutInfo = listState.layoutInfo
 | 
			
		||||
            val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount
 | 
			
		||||
            if (!showScroller) return@subcompose
 | 
			
		||||
 | 
			
		||||
            val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
 | 
			
		||||
            var thumbOffsetY by remember(thumbTopPadding) { mutableStateOf(thumbTopPadding) }
 | 
			
		||||
 | 
			
		||||
            val dragInteractionSource = remember { MutableInteractionSource() }
 | 
			
		||||
            val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
 | 
			
		||||
            val scrolled = remember {
 | 
			
		||||
                MutableSharedFlow<Unit>(
 | 
			
		||||
                    extraBufferCapacity = 1,
 | 
			
		||||
                    onBufferOverflow = BufferOverflow.DROP_OLDEST,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() }
 | 
			
		||||
            val heightPx = contentHeight.toFloat() - thumbTopPadding - thumbBottomPadding - listState.layoutInfo.afterContentPadding
 | 
			
		||||
            val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
 | 
			
		||||
            val trackHeightPx = heightPx - thumbHeightPx
 | 
			
		||||
 | 
			
		||||
            // When thumb dragged
 | 
			
		||||
            LaunchedEffect(thumbOffsetY) {
 | 
			
		||||
                if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
 | 
			
		||||
                val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx
 | 
			
		||||
                val scrollItem = layoutInfo.totalItemsCount * scrollRatio
 | 
			
		||||
                val scrollItemRounded = scrollItem.roundToInt()
 | 
			
		||||
                val scrollItemSize = layoutInfo.visibleItemsInfo.find { it.index == scrollItemRounded }?.size ?: 0
 | 
			
		||||
                val scrollItemOffset = scrollItemSize * (scrollItem - scrollItemRounded)
 | 
			
		||||
                listState.scrollToItem(index = scrollItemRounded, scrollOffset = scrollItemOffset.roundToInt())
 | 
			
		||||
                scrolled.tryEmit(Unit)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // When list scrolled
 | 
			
		||||
            LaunchedEffect(listState.firstVisibleItemScrollOffset) {
 | 
			
		||||
                if (listState.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect
 | 
			
		||||
                val scrollOffset = computeScrollOffset(state = listState)
 | 
			
		||||
                val scrollRange = computeScrollRange(state = listState)
 | 
			
		||||
                val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx)
 | 
			
		||||
                thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
 | 
			
		||||
                scrolled.tryEmit(Unit)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Thumb alpha
 | 
			
		||||
            val alpha = remember { Animatable(0f) }
 | 
			
		||||
            val isThumbVisible = alpha.value > 0f
 | 
			
		||||
            LaunchedEffect(scrolled, alpha) {
 | 
			
		||||
                scrolled.collectLatest {
 | 
			
		||||
                    if (thumbAllowed()) {
 | 
			
		||||
                        alpha.snapTo(1f)
 | 
			
		||||
                        alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .offset { IntOffset(0, thumbOffsetY.roundToInt()) }
 | 
			
		||||
                    .then(
 | 
			
		||||
                        // Recompose opts
 | 
			
		||||
                        if (isThumbVisible && !listState.isScrollInProgress) {
 | 
			
		||||
                            Modifier.draggable(
 | 
			
		||||
                                interactionSource = dragInteractionSource,
 | 
			
		||||
                                orientation = Orientation.Vertical,
 | 
			
		||||
                                state = rememberDraggableState { delta ->
 | 
			
		||||
                                    val newOffsetY = thumbOffsetY + delta
 | 
			
		||||
                                    thumbOffsetY = newOffsetY.coerceIn(
 | 
			
		||||
                                        thumbTopPadding,
 | 
			
		||||
                                        thumbTopPadding + trackHeightPx,
 | 
			
		||||
                                    )
 | 
			
		||||
                                },
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Modifier
 | 
			
		||||
                        },
 | 
			
		||||
                    )
 | 
			
		||||
                    .then(
 | 
			
		||||
                        // Exclude thumb from gesture area only when needed
 | 
			
		||||
                        if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) {
 | 
			
		||||
                            Modifier.systemGestureExclusion()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Modifier
 | 
			
		||||
                        },
 | 
			
		||||
                    )
 | 
			
		||||
                    .height(ThumbLength)
 | 
			
		||||
                    .padding(horizontal = 8.dp)
 | 
			
		||||
                    .padding(end = endContentPadding)
 | 
			
		||||
                    .width(ThumbThickness)
 | 
			
		||||
                    .alpha(alpha.value)
 | 
			
		||||
                    .background(color = thumbColor, shape = ThumbShape),
 | 
			
		||||
            )
 | 
			
		||||
        }.map { it.measure(scrollerConstraints) }
 | 
			
		||||
        val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
 | 
			
		||||
 | 
			
		||||
        layout(contentWidth, contentHeight) {
 | 
			
		||||
            contentPlaceable.fastForEach {
 | 
			
		||||
                it.place(0, 0)
 | 
			
		||||
            }
 | 
			
		||||
            scrollerPlaceable.fastForEach {
 | 
			
		||||
                it.placeRelative(contentWidth - scrollerWidth, 0)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun rememberColumnWidthSums(
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    horizontalArrangement: Arrangement.Horizontal,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
) = remember<Density.(Constraints) -> List<Int>>(
 | 
			
		||||
    columns,
 | 
			
		||||
    horizontalArrangement,
 | 
			
		||||
    contentPadding,
 | 
			
		||||
) {
 | 
			
		||||
    { constraints ->
 | 
			
		||||
        require(constraints.maxWidth != Constraints.Infinity) {
 | 
			
		||||
            "LazyVerticalGrid's width should be bound by parent"
 | 
			
		||||
        }
 | 
			
		||||
        val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
 | 
			
		||||
            contentPadding.calculateEndPadding(LayoutDirection.Ltr)
 | 
			
		||||
        val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
 | 
			
		||||
        with(columns) {
 | 
			
		||||
            calculateCrossAxisCellSizes(
 | 
			
		||||
                gridWidth,
 | 
			
		||||
                horizontalArrangement.spacing.roundToPx(),
 | 
			
		||||
            ).toMutableList().apply {
 | 
			
		||||
                for (i in 1 until size) {
 | 
			
		||||
                    this[i] += this[i - 1]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun VerticalGridFastScroller(
 | 
			
		||||
    state: LazyGridState,
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    arrangement: Arrangement.Horizontal,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    thumbAllowed: () -> Boolean = { true },
 | 
			
		||||
    thumbColor: Color = MaterialTheme.colorScheme.primary,
 | 
			
		||||
    topContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    bottomContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    endContentPadding: Dp = Dp.Hairline,
 | 
			
		||||
    content: @Composable () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val slotSizesSums = rememberColumnWidthSums(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
        horizontalArrangement = arrangement,
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    SubcomposeLayout(modifier = modifier) { constraints ->
 | 
			
		||||
        val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
 | 
			
		||||
        val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
 | 
			
		||||
        val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
 | 
			
		||||
 | 
			
		||||
        val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
 | 
			
		||||
        val scrollerPlaceable = subcompose("scroller") {
 | 
			
		||||
            val layoutInfo = state.layoutInfo
 | 
			
		||||
            val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount
 | 
			
		||||
            if (!showScroller) return@subcompose
 | 
			
		||||
            val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
 | 
			
		||||
            var thumbOffsetY by remember(thumbTopPadding) { mutableStateOf(thumbTopPadding) }
 | 
			
		||||
 | 
			
		||||
            val dragInteractionSource = remember { MutableInteractionSource() }
 | 
			
		||||
            val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
 | 
			
		||||
            val scrolled = remember {
 | 
			
		||||
                MutableSharedFlow<Unit>(
 | 
			
		||||
                    extraBufferCapacity = 1,
 | 
			
		||||
                    onBufferOverflow = BufferOverflow.DROP_OLDEST,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() }
 | 
			
		||||
            val heightPx = contentHeight.toFloat() - thumbTopPadding - thumbBottomPadding - state.layoutInfo.afterContentPadding
 | 
			
		||||
            val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
 | 
			
		||||
            val trackHeightPx = heightPx - thumbHeightPx
 | 
			
		||||
 | 
			
		||||
            val columnCount = remember { slotSizesSums(constraints).size }
 | 
			
		||||
 | 
			
		||||
            // When thumb dragged
 | 
			
		||||
            LaunchedEffect(thumbOffsetY) {
 | 
			
		||||
                if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
 | 
			
		||||
                val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx
 | 
			
		||||
                val scrollItem = layoutInfo.totalItemsCount * scrollRatio
 | 
			
		||||
                // I can't think of anything else rn but this'll do
 | 
			
		||||
                val scrollItemWhole = scrollItem.toInt()
 | 
			
		||||
                val columnNum = ((scrollItemWhole + 1) % columnCount).takeIf { it != 0 } ?: columnCount
 | 
			
		||||
                val scrollItemFraction = if (scrollItemWhole == 0) scrollItem else scrollItem % scrollItemWhole
 | 
			
		||||
                val offsetPerItem = 1f / columnCount
 | 
			
		||||
                val offsetRatio = (offsetPerItem * scrollItemFraction) + (offsetPerItem * (columnNum - 1))
 | 
			
		||||
 | 
			
		||||
                // TODO: Sometimes item height is not available when scrolling up
 | 
			
		||||
                val scrollItemSize = (1..columnCount).maxOf { num ->
 | 
			
		||||
                    val actualIndex = if (num != columnNum) {
 | 
			
		||||
                        scrollItemWhole + num - columnCount
 | 
			
		||||
                    } else {
 | 
			
		||||
                        scrollItemWhole
 | 
			
		||||
                    }
 | 
			
		||||
                    layoutInfo.visibleItemsInfo.find { it.index == actualIndex }?.size?.height ?: 0
 | 
			
		||||
                }
 | 
			
		||||
                val scrollItemOffset = scrollItemSize * offsetRatio
 | 
			
		||||
 | 
			
		||||
                state.scrollToItem(index = scrollItemWhole, scrollOffset = scrollItemOffset.roundToInt())
 | 
			
		||||
                scrolled.tryEmit(Unit)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // When list scrolled
 | 
			
		||||
            LaunchedEffect(state.firstVisibleItemScrollOffset) {
 | 
			
		||||
                if (state.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect
 | 
			
		||||
                val scrollOffset = computeScrollOffset(state = state)
 | 
			
		||||
                val scrollRange = computeScrollRange(state = state)
 | 
			
		||||
                val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx)
 | 
			
		||||
                thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
 | 
			
		||||
                scrolled.tryEmit(Unit)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Thumb alpha
 | 
			
		||||
            val alpha = remember { Animatable(0f) }
 | 
			
		||||
            val isThumbVisible = alpha.value > 0f
 | 
			
		||||
            LaunchedEffect(scrolled, alpha) {
 | 
			
		||||
                scrolled.collectLatest {
 | 
			
		||||
                    if (thumbAllowed()) {
 | 
			
		||||
                        alpha.snapTo(1f)
 | 
			
		||||
                        alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .offset { IntOffset(0, thumbOffsetY.roundToInt()) }
 | 
			
		||||
                    .then(
 | 
			
		||||
                        // Recompose opts
 | 
			
		||||
                        if (isThumbVisible && !state.isScrollInProgress) {
 | 
			
		||||
                            Modifier.draggable(
 | 
			
		||||
                                interactionSource = dragInteractionSource,
 | 
			
		||||
                                orientation = Orientation.Vertical,
 | 
			
		||||
                                state = rememberDraggableState { delta ->
 | 
			
		||||
                                    val newOffsetY = thumbOffsetY + delta
 | 
			
		||||
                                    thumbOffsetY = newOffsetY.coerceIn(
 | 
			
		||||
                                        thumbTopPadding,
 | 
			
		||||
                                        thumbTopPadding + trackHeightPx,
 | 
			
		||||
                                    )
 | 
			
		||||
                                },
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Modifier
 | 
			
		||||
                        },
 | 
			
		||||
                    )
 | 
			
		||||
                    .then(
 | 
			
		||||
                        // Exclude thumb from gesture area only when needed
 | 
			
		||||
                        if (isThumbVisible && !isThumbDragged && !state.isScrollInProgress) {
 | 
			
		||||
                            Modifier.systemGestureExclusion()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Modifier
 | 
			
		||||
                        },
 | 
			
		||||
                    )
 | 
			
		||||
                    .height(ThumbLength)
 | 
			
		||||
                    .padding(horizontal = 8.dp)
 | 
			
		||||
                    .padding(end = endContentPadding)
 | 
			
		||||
                    .width(ThumbThickness)
 | 
			
		||||
                    .alpha(alpha.value)
 | 
			
		||||
                    .background(color = thumbColor, shape = ThumbShape),
 | 
			
		||||
            )
 | 
			
		||||
        }.map { it.measure(scrollerConstraints) }
 | 
			
		||||
        val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
 | 
			
		||||
 | 
			
		||||
        layout(contentWidth, contentHeight) {
 | 
			
		||||
            contentPlaceable.fastForEach {
 | 
			
		||||
                it.place(0, 0)
 | 
			
		||||
            }
 | 
			
		||||
            scrollerPlaceable.fastForEach {
 | 
			
		||||
                it.placeRelative(contentWidth - scrollerWidth, 0)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun computeScrollOffset(state: LazyGridState): Int {
 | 
			
		||||
    if (state.layoutInfo.totalItemsCount == 0) return 0
 | 
			
		||||
    val visibleItems = state.layoutInfo.visibleItemsInfo
 | 
			
		||||
    val startChild = visibleItems.first()
 | 
			
		||||
    val endChild = visibleItems.last()
 | 
			
		||||
    val minPosition = min(startChild.index, endChild.index)
 | 
			
		||||
    val maxPosition = max(startChild.index, endChild.index)
 | 
			
		||||
    val itemsBefore = minPosition.coerceAtLeast(0)
 | 
			
		||||
    val startDecoratedTop = startChild.offset.y
 | 
			
		||||
    val laidOutArea = abs((endChild.offset.y + endChild.size.height) - startDecoratedTop)
 | 
			
		||||
    val itemRange = abs(minPosition - maxPosition) + 1
 | 
			
		||||
    val avgSizePerRow = laidOutArea.toFloat() / itemRange
 | 
			
		||||
    return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun computeScrollRange(state: LazyGridState): Int {
 | 
			
		||||
    if (state.layoutInfo.totalItemsCount == 0) return 0
 | 
			
		||||
    val visibleItems = state.layoutInfo.visibleItemsInfo
 | 
			
		||||
    val startChild = visibleItems.first()
 | 
			
		||||
    val endChild = visibleItems.last()
 | 
			
		||||
    val laidOutArea = (endChild.offset.y + endChild.size.height) - startChild.offset.y
 | 
			
		||||
    val laidOutRange = abs(startChild.index - endChild.index) + 1
 | 
			
		||||
    return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun computeScrollOffset(state: LazyListState): Int {
 | 
			
		||||
    if (state.layoutInfo.totalItemsCount == 0) return 0
 | 
			
		||||
    val visibleItems = state.layoutInfo.visibleItemsInfo
 | 
			
		||||
    val startChild = visibleItems
 | 
			
		||||
        .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
 | 
			
		||||
    val endChild = visibleItems.last()
 | 
			
		||||
    val minPosition = min(startChild.index, endChild.index)
 | 
			
		||||
    val maxPosition = max(startChild.index, endChild.index)
 | 
			
		||||
    val itemsBefore = minPosition.coerceAtLeast(0)
 | 
			
		||||
    val startDecoratedTop = startChild.top
 | 
			
		||||
    val laidOutArea = abs(endChild.bottom - startDecoratedTop)
 | 
			
		||||
    val itemRange = abs(minPosition - maxPosition) + 1
 | 
			
		||||
    val avgSizePerRow = laidOutArea.toFloat() / itemRange
 | 
			
		||||
    return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun computeScrollRange(state: LazyListState): Int {
 | 
			
		||||
    if (state.layoutInfo.totalItemsCount == 0) return 0
 | 
			
		||||
    val visibleItems = state.layoutInfo.visibleItemsInfo
 | 
			
		||||
    val startChild = visibleItems
 | 
			
		||||
        .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
 | 
			
		||||
    val endChild = visibleItems.last()
 | 
			
		||||
    val laidOutArea = endChild.bottom - startChild.top
 | 
			
		||||
    val laidOutRange = abs(startChild.index - endChild.index) + 1
 | 
			
		||||
    return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object Scroller {
 | 
			
		||||
    const val STICKY_HEADER_KEY_PREFIX = "sticky:"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val ThumbLength = 48.dp
 | 
			
		||||
private val ThumbThickness = 8.dp
 | 
			
		||||
private val ThumbShape = RoundedCornerShape(ThumbThickness / 2)
 | 
			
		||||
private val FadeOutAnimationSpec = tween<Float>(
 | 
			
		||||
    durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
 | 
			
		||||
    delayMillis = 2000,
 | 
			
		||||
)
 | 
			
		||||
private val ImmediateFadeOutAnimationSpec = tween<Float>(
 | 
			
		||||
    durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
private val LazyListItemInfo.top: Int
 | 
			
		||||
    get() = offset
 | 
			
		||||
 | 
			
		||||
private val LazyListItemInfo.bottom: Int
 | 
			
		||||
    get() = offset + size
 | 
			
		||||
@@ -1,286 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.BorderStroke
 | 
			
		||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyItemScope
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListItemInfo
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.foundation.lazy.rememberLazyListState
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
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.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.unit.DpSize
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.distinctUntilChanged
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import java.text.DateFormatSymbols
 | 
			
		||||
import java.time.LocalDate
 | 
			
		||||
import kotlin.math.absoluteValue
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun WheelPicker(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    startIndex: Int = 0,
 | 
			
		||||
    count: Int,
 | 
			
		||||
    size: DpSize = DpSize(128.dp, 128.dp),
 | 
			
		||||
    onSelectionChanged: (index: Int) -> Unit = {},
 | 
			
		||||
    backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
 | 
			
		||||
        WheelPickerDefaults.Background(size = it)
 | 
			
		||||
    },
 | 
			
		||||
    itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val lazyListState = rememberLazyListState(startIndex)
 | 
			
		||||
 | 
			
		||||
    LaunchedEffect(lazyListState, onSelectionChanged) {
 | 
			
		||||
        snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
 | 
			
		||||
            .map { calculateSnappedItemIndex(lazyListState) }
 | 
			
		||||
            .distinctUntilChanged()
 | 
			
		||||
            .collectLatest {
 | 
			
		||||
                onSelectionChanged(it)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        contentAlignment = Alignment.Center,
 | 
			
		||||
    ) {
 | 
			
		||||
        backgroundContent?.invoke(size)
 | 
			
		||||
 | 
			
		||||
        LazyColumn(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .height(size.height)
 | 
			
		||||
                .width(size.width),
 | 
			
		||||
            state = lazyListState,
 | 
			
		||||
            contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)),
 | 
			
		||||
            flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
 | 
			
		||||
        ) {
 | 
			
		||||
            items(count) { index ->
 | 
			
		||||
                Box(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .height(size.height / RowCount)
 | 
			
		||||
                        .width(size.width)
 | 
			
		||||
                        .alpha(
 | 
			
		||||
                            calculateAnimatedAlpha(
 | 
			
		||||
                                lazyListState = lazyListState,
 | 
			
		||||
                                index = index,
 | 
			
		||||
                            ),
 | 
			
		||||
                        ),
 | 
			
		||||
                    contentAlignment = Alignment.Center,
 | 
			
		||||
                ) {
 | 
			
		||||
                    itemContent(index)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun WheelTextPicker(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    startIndex: Int = 0,
 | 
			
		||||
    texts: List<String>,
 | 
			
		||||
    size: DpSize = DpSize(128.dp, 128.dp),
 | 
			
		||||
    onSelectionChanged: (index: Int) -> Unit = {},
 | 
			
		||||
    backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
 | 
			
		||||
        WheelPickerDefaults.Background(size = it)
 | 
			
		||||
    },
 | 
			
		||||
) {
 | 
			
		||||
    WheelPicker(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        startIndex = startIndex,
 | 
			
		||||
        count = remember(texts) { texts.size },
 | 
			
		||||
        size = size,
 | 
			
		||||
        onSelectionChanged = onSelectionChanged,
 | 
			
		||||
        backgroundContent = backgroundContent,
 | 
			
		||||
    ) {
 | 
			
		||||
        WheelPickerDefaults.Item(text = texts[it])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun WheelDatePicker(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    startDate: LocalDate = LocalDate.now(),
 | 
			
		||||
    minDate: LocalDate? = null,
 | 
			
		||||
    maxDate: LocalDate? = null,
 | 
			
		||||
    size: DpSize = DpSize(256.dp, 128.dp),
 | 
			
		||||
    backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
 | 
			
		||||
        WheelPickerDefaults.Background(size = it)
 | 
			
		||||
    },
 | 
			
		||||
    onSelectionChanged: (date: LocalDate) -> Unit = {},
 | 
			
		||||
) {
 | 
			
		||||
    var internalSelection by remember { mutableStateOf(startDate) }
 | 
			
		||||
    val internalOnSelectionChange: (LocalDate) -> Unit = {
 | 
			
		||||
        internalSelection = it
 | 
			
		||||
        onSelectionChanged(internalSelection)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Box(modifier = modifier, contentAlignment = Alignment.Center) {
 | 
			
		||||
        backgroundContent?.invoke(size)
 | 
			
		||||
        Row {
 | 
			
		||||
            val singularPickerSize = DpSize(
 | 
			
		||||
                width = size.width / 3,
 | 
			
		||||
                height = size.height,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            // Day
 | 
			
		||||
            val dayOfMonths = remember(internalSelection, minDate, maxDate) {
 | 
			
		||||
                if (minDate == null && maxDate == null) {
 | 
			
		||||
                    1..internalSelection.lengthOfMonth()
 | 
			
		||||
                } else {
 | 
			
		||||
                    val minDay = if (minDate?.month == internalSelection.month &&
 | 
			
		||||
                        minDate?.year == internalSelection.year
 | 
			
		||||
                    ) {
 | 
			
		||||
                        minDate.dayOfMonth
 | 
			
		||||
                    } else {
 | 
			
		||||
                        1
 | 
			
		||||
                    }
 | 
			
		||||
                    val maxDay = if (maxDate?.month == internalSelection.month &&
 | 
			
		||||
                        maxDate?.year == internalSelection.year
 | 
			
		||||
                    ) {
 | 
			
		||||
                        maxDate.dayOfMonth
 | 
			
		||||
                    } else {
 | 
			
		||||
                        31
 | 
			
		||||
                    }
 | 
			
		||||
                    minDay..maxDay.coerceAtMost(internalSelection.lengthOfMonth())
 | 
			
		||||
                }.toList()
 | 
			
		||||
            }
 | 
			
		||||
            WheelTextPicker(
 | 
			
		||||
                size = singularPickerSize,
 | 
			
		||||
                texts = dayOfMonths.map { it.toString() },
 | 
			
		||||
                backgroundContent = null,
 | 
			
		||||
                startIndex = dayOfMonths.indexOfFirst { it == startDate.dayOfMonth }.coerceAtLeast(0),
 | 
			
		||||
                onSelectionChanged = { index ->
 | 
			
		||||
                    val newDayOfMonth = dayOfMonths[index]
 | 
			
		||||
                    internalOnSelectionChange(internalSelection.withDayOfMonth(newDayOfMonth))
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            // Month
 | 
			
		||||
            val months = remember(internalSelection, minDate, maxDate) {
 | 
			
		||||
                val monthRange = if (minDate == null && maxDate == null) {
 | 
			
		||||
                    1..12
 | 
			
		||||
                } else {
 | 
			
		||||
                    val minMonth = if (minDate?.year == internalSelection.year) {
 | 
			
		||||
                        minDate.monthValue
 | 
			
		||||
                    } else {
 | 
			
		||||
                        1
 | 
			
		||||
                    }
 | 
			
		||||
                    val maxMonth = if (maxDate?.year == internalSelection.year) {
 | 
			
		||||
                        maxDate.monthValue
 | 
			
		||||
                    } else {
 | 
			
		||||
                        12
 | 
			
		||||
                    }
 | 
			
		||||
                    minMonth..maxMonth
 | 
			
		||||
                }
 | 
			
		||||
                val dateFormatSymbols = DateFormatSymbols()
 | 
			
		||||
                monthRange.map { it to dateFormatSymbols.months[it - 1] }
 | 
			
		||||
            }
 | 
			
		||||
            WheelTextPicker(
 | 
			
		||||
                size = singularPickerSize,
 | 
			
		||||
                texts = months.map { it.second },
 | 
			
		||||
                backgroundContent = null,
 | 
			
		||||
                startIndex = months.indexOfFirst { it.first == startDate.monthValue }.coerceAtLeast(0),
 | 
			
		||||
                onSelectionChanged = { index ->
 | 
			
		||||
                    val newMonth = months[index].first
 | 
			
		||||
                    internalOnSelectionChange(internalSelection.withMonth(newMonth))
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            // Year
 | 
			
		||||
            val years = remember(minDate, maxDate) {
 | 
			
		||||
                val minYear = minDate?.year?.coerceAtLeast(1900) ?: 1900
 | 
			
		||||
                val maxYear = maxDate?.year?.coerceAtMost(2100) ?: 2100
 | 
			
		||||
                val yearRange = minYear..maxYear
 | 
			
		||||
                yearRange.toList()
 | 
			
		||||
            }
 | 
			
		||||
            WheelTextPicker(
 | 
			
		||||
                size = singularPickerSize,
 | 
			
		||||
                texts = years.map { it.toString() },
 | 
			
		||||
                backgroundContent = null,
 | 
			
		||||
                startIndex = years.indexOfFirst { it == startDate.year }.coerceAtLeast(0),
 | 
			
		||||
                onSelectionChanged = { index ->
 | 
			
		||||
                    val newYear = years[index]
 | 
			
		||||
                    internalOnSelectionChange(internalSelection.withYear(newYear))
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun LazyListState.snapOffsetForItem(itemInfo: LazyListItemInfo): Int {
 | 
			
		||||
    val startScrollOffset = 0
 | 
			
		||||
    val endScrollOffset = layoutInfo.let { it.viewportEndOffset - it.afterContentPadding }
 | 
			
		||||
    return startScrollOffset + (endScrollOffset - startScrollOffset - itemInfo.size) / 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun LazyListState.distanceToSnapForIndex(index: Int): Int {
 | 
			
		||||
    val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
 | 
			
		||||
    if (itemInfo != null) {
 | 
			
		||||
        return itemInfo.offset - snapOffsetForItem(itemInfo)
 | 
			
		||||
    }
 | 
			
		||||
    return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun calculateAnimatedAlpha(
 | 
			
		||||
    lazyListState: LazyListState,
 | 
			
		||||
    index: Int,
 | 
			
		||||
): Float {
 | 
			
		||||
    val distanceToIndexSnap = lazyListState.distanceToSnapForIndex(index).absoluteValue
 | 
			
		||||
    val viewPortHeight = lazyListState.layoutInfo.viewportSize.height.toFloat()
 | 
			
		||||
    val singleViewPortHeight = viewPortHeight / RowCount
 | 
			
		||||
    return if (distanceToIndexSnap in 0..singleViewPortHeight.toInt()) {
 | 
			
		||||
        1.2f - (distanceToIndexSnap / singleViewPortHeight)
 | 
			
		||||
    } else {
 | 
			
		||||
        0.2f
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun calculateSnappedItemIndex(lazyListState: LazyListState): Int {
 | 
			
		||||
    return lazyListState.layoutInfo.visibleItemsInfo
 | 
			
		||||
        .maxBy { calculateAnimatedAlpha(lazyListState, it.index) }
 | 
			
		||||
        .index
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object WheelPickerDefaults {
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun Background(size: DpSize) {
 | 
			
		||||
        androidx.compose.material3.Surface(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .size(size.width, size.height / RowCount),
 | 
			
		||||
            shape = RoundedCornerShape(MaterialTheme.padding.medium),
 | 
			
		||||
            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
 | 
			
		||||
            border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
 | 
			
		||||
            content = {},
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun Item(text: String) {
 | 
			
		||||
        Text(
 | 
			
		||||
            text = text,
 | 
			
		||||
            style = MaterialTheme.typography.titleMedium,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private const val RowCount = 3
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.components.dialogs
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
@@ -25,6 +25,7 @@ import eu.kanade.core.prefs.CheckboxState
 | 
			
		||||
import eu.kanade.presentation.category.visualName
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.domain.category.model.Category
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TextButton
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.components.dialogs
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.components.dialogs
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
@@ -16,11 +16,11 @@ 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.presentation.util.ThemePreviews
 | 
			
		||||
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.util.ThemePreviews
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CrashScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import eu.kanade.domain.ui.UiPreferences
 | 
			
		||||
import eu.kanade.presentation.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.RelativeDateHeader
 | 
			
		||||
import eu.kanade.presentation.history.HistoryUiModel
 | 
			
		||||
import tachiyomi.domain.history.model.HistoryWithRelations
 | 
			
		||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaCover
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.toTimestampString
 | 
			
		||||
import tachiyomi.domain.history.model.HistoryWithRelations
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
@@ -39,8 +39,9 @@ import androidx.compose.ui.text.TextStyle
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.unit.sp
 | 
			
		||||
import eu.kanade.presentation.util.selectedBackground
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaCover
 | 
			
		||||
import tachiyomi.presentation.core.components.BadgeGroup
 | 
			
		||||
import tachiyomi.presentation.core.util.selectedBackground
 | 
			
		||||
 | 
			
		||||
object CommonMangaItemDefaults {
 | 
			
		||||
    val GridHorizontalSpacer = 4.dp
 | 
			
		||||
@@ -8,9 +8,8 @@ import androidx.compose.foundation.lazy.grid.LazyGridScope
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.FastScrollLazyVerticalGrid
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import tachiyomi.presentation.core.components.FastScrollLazyVerticalGrid
 | 
			
		||||
import tachiyomi.presentation.core.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyLibraryGrid(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ import androidx.compose.material.icons.outlined.Folder
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.presentation.util.ThemePreviews
 | 
			
		||||
import tachiyomi.presentation.core.components.Badge
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun DownloadsBadge(count: Long) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.lazy.grid.items
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.util.fastAny
 | 
			
		||||
import eu.kanade.presentation.components.MangaComfortableGridItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
import tachiyomi.domain.library.model.LibraryManga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.lazy.grid.items
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.util.fastAny
 | 
			
		||||
import eu.kanade.presentation.components.MangaCompactGridItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
import tachiyomi.domain.library.model.LibraryManga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,11 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.util.fastAny
 | 
			
		||||
import eu.kanade.presentation.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaListItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
import tachiyomi.domain.library.model.LibraryManga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaCover
 | 
			
		||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryList(
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,11 @@ import eu.kanade.core.prefs.PreferenceMutableState
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.HorizontalPager
 | 
			
		||||
import eu.kanade.presentation.components.PagerState
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
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.util.plus
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryPager(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,10 @@ import androidx.compose.material3.Tab
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.category.visualName
 | 
			
		||||
import eu.kanade.presentation.components.TabIndicator
 | 
			
		||||
import eu.kanade.presentation.components.TabText
 | 
			
		||||
import tachiyomi.domain.category.model.Category
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TabIndicator
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TabText
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryTabs(
 | 
			
		||||
 
 | 
			
		||||
@@ -48,18 +48,14 @@ import androidx.compose.ui.util.fastAll
 | 
			
		||||
import androidx.compose.ui.util.fastAny
 | 
			
		||||
import androidx.compose.ui.util.fastMap
 | 
			
		||||
import eu.kanade.domain.manga.model.chaptersFiltered
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaBottomActionMenu
 | 
			
		||||
import eu.kanade.presentation.components.VerticalFastScroller
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterHeader
 | 
			
		||||
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaActionRow
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaChapterListItem
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaInfoBox
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaToolbar
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToEnd
 | 
			
		||||
import eu.kanade.presentation.util.isScrollingUp
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
@@ -71,10 +67,14 @@ import eu.kanade.tachiyomi.util.lang.toRelativeString
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
 | 
			
		||||
import tachiyomi.domain.chapter.model.Chapter
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.TwoPanelBox
 | 
			
		||||
import tachiyomi.presentation.core.components.VerticalFastScroller
 | 
			
		||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
 | 
			
		||||
import tachiyomi.presentation.core.components.material.PullRefresh
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrollingUp
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.manga.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.core.animateFloatAsState
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
@@ -32,10 +32,11 @@ import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.semantics.Role
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import tachiyomi.presentation.core.components.material.IconButtonTokens
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
enum class ChapterDownloadAction {
 | 
			
		||||
    START,
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.manga.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.animation.core.animateFloatAsState
 | 
			
		||||
@@ -51,6 +51,7 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.res.vectorResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.DownloadDropdownMenu
 | 
			
		||||
import eu.kanade.presentation.manga.DownloadAction
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
@@ -27,13 +27,11 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.unit.sp
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
 | 
			
		||||
import eu.kanade.presentation.util.selectedBackground
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import tachiyomi.presentation.core.components.material.ReadItemAlpha
 | 
			
		||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
 | 
			
		||||
import tachiyomi.presentation.core.util.selectedBackground
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MangaChapterListItem(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.manga.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.aspectRatio
 | 
			
		||||
@@ -45,11 +45,11 @@ import coil.request.ImageRequest
 | 
			
		||||
import coil.size.Size
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.presentation.manga.EditCoverAction
 | 
			
		||||
import eu.kanade.presentation.util.clickableNoIndication
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
import tachiyomi.presentation.core.util.clickableNoIndication
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MangaCoverDialog(
 | 
			
		||||
 
 | 
			
		||||
@@ -74,15 +74,14 @@ import androidx.compose.ui.unit.sp
 | 
			
		||||
import coil.compose.AsyncImage
 | 
			
		||||
import com.google.accompanist.flowlayout.FlowRow
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.components.TextButton
 | 
			
		||||
import eu.kanade.presentation.util.clickableNoIndication
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TextButton
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.clickableNoIndication
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
 | 
			
		||||
 
 | 
			
		||||
@@ -24,13 +24,13 @@ import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.compose.ui.res.pluralStringResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.res.vectorResource
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.WarningBanner
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
 | 
			
		||||
import tachiyomi.core.Constants
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,9 @@ 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.presentation.util.ThemePreviews
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun NewUpdateScreen(
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,10 @@ import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.util.fastForEachIndexed
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.more.settings.screen.SearchableSettings
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.PreferenceGroupHeader
 | 
			
		||||
import kotlinx.coroutines.delay
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import kotlin.time.Duration.Companion.seconds
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.domain.ui.UiPreferences
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.more.LogoHeader
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 | 
			
		||||
import eu.kanade.presentation.util.LocalBackPress
 | 
			
		||||
@@ -41,6 +40,7 @@ import tachiyomi.core.util.lang.withIOContext
 | 
			
		||||
import tachiyomi.core.util.lang.withUIContext
 | 
			
		||||
import tachiyomi.core.util.system.logcat
 | 
			
		||||
import tachiyomi.presentation.core.components.LinkIcon
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 
 | 
			
		||||
@@ -39,8 +39,6 @@ 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.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.util.selectedBackground
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
@@ -51,9 +49,11 @@ import tachiyomi.core.util.lang.withNonCancellableContext
 | 
			
		||||
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.util.selectedBackground
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,11 +37,8 @@ import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.core.net.toUri
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.domain.backup.service.BackupPreferences
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.more.settings.Preference
 | 
			
		||||
import eu.kanade.presentation.util.collectAsState
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToEnd
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToStart
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.BackupConst
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
 | 
			
		||||
@@ -53,7 +50,10 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToStart
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.domain.category.interactor.ResetCategoryFlags
 | 
			
		||||
import eu.kanade.domain.library.service.LibraryPreferences
 | 
			
		||||
import eu.kanade.presentation.category.visualName
 | 
			
		||||
import eu.kanade.presentation.components.WheelPicker
 | 
			
		||||
import eu.kanade.presentation.components.WheelPickerDefaults
 | 
			
		||||
import eu.kanade.presentation.more.settings.Preference
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
 | 
			
		||||
import eu.kanade.presentation.util.collectAsState
 | 
			
		||||
@@ -54,6 +52,8 @@ import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import tachiyomi.domain.category.interactor.GetCategories
 | 
			
		||||
import tachiyomi.domain.category.model.Category
 | 
			
		||||
import tachiyomi.presentation.core.components.WheelPicker
 | 
			
		||||
import tachiyomi.presentation.core.components.WheelPickerDefaults
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -47,10 +47,10 @@ import cafe.adriel.voyager.navigator.Navigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.AppBarActions
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 | 
			
		||||
import eu.kanade.presentation.util.LocalBackPress
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
 | 
			
		||||
object SettingsMainScreen : Screen {
 | 
			
		||||
 
 | 
			
		||||
@@ -52,11 +52,11 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.more.settings.Preference
 | 
			
		||||
import eu.kanade.presentation.util.runOnEnterKeyPressed
 | 
			
		||||
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.util.runOnEnterKeyPressed
 | 
			
		||||
 | 
			
		||||
class SettingsSearchScreen : Screen {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,14 +37,14 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
 | 
			
		||||
import cafe.adriel.voyager.core.screen.Screen
 | 
			
		||||
import cafe.adriel.voyager.navigator.LocalNavigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Scaffold
 | 
			
		||||
import tachiyomi.presentation.core.util.plus
 | 
			
		||||
 | 
			
		||||
object WorkerInfoScreen : Screen {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,15 +39,15 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextAlign
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.domain.ui.model.AppTheme
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.presentation.util.ThemePreviews
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
 | 
			
		||||
import tachiyomi.presentation.core.components.material.DIVIDER_ALPHA
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
internal fun AppThemePreferenceWidget(
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.presentation.util.ThemePreviews
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
internal fun InfoWidget(text: String) {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,12 @@ import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.graphics.vector.ImageVector
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToEnd
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToStart
 | 
			
		||||
import eu.kanade.presentation.util.minimumTouchTargetSize
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToStart
 | 
			
		||||
import tachiyomi.presentation.core.util.minimumTouchTargetSize
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun <T> ListPreferenceWidget(
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.window.DialogProperties
 | 
			
		||||
import eu.kanade.presentation.more.settings.Preference
 | 
			
		||||
import eu.kanade.presentation.util.minimumTouchTargetSize
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.util.minimumTouchTargetSize
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MultiSelectListPreferenceWidget(
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.vector.ImageVector
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.presentation.util.ThemePreviews
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SwitchPreferenceWidget(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,9 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.vector.ImageVector
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.presentation.util.ThemePreviews
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import tachiyomi.presentation.core.util.ThemePreviews
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TextPreferenceWidget(
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,8 @@ import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.TrackLogoIcon
 | 
			
		||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
 | 
			
		||||
import eu.kanade.presentation.track.components.TrackLogoIcon
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,11 +27,11 @@ import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToEnd
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToStart
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToStart
 | 
			
		||||
 | 
			
		||||
private enum class State {
 | 
			
		||||
    CHECKED, INVERSED, UNCHECKED
 | 
			
		||||
 
 | 
			
		||||
@@ -14,12 +14,12 @@ import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.core.util.toDurationString
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.more.stats.components.StatsItem
 | 
			
		||||
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
 | 
			
		||||
import eu.kanade.presentation.more.stats.components.StatsSection
 | 
			
		||||
import eu.kanade.presentation.more.stats.data.StatsData
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.LazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
import kotlin.time.DurationUnit
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.manga
 | 
			
		||||
package eu.kanade.presentation.track
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.animateContentSize
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
@@ -43,7 +43,7 @@ import androidx.compose.ui.text.style.TextAlign
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.presentation.components.TrackLogoIcon
 | 
			
		||||
import eu.kanade.presentation.track.components.TrackLogoIcon
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.manga
 | 
			
		||||
package eu.kanade.presentation.track
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
@@ -29,18 +29,19 @@ import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextAlign
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.WheelDatePicker
 | 
			
		||||
import eu.kanade.presentation.components.WheelTextPicker
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToEnd
 | 
			
		||||
import eu.kanade.presentation.util.isScrolledToStart
 | 
			
		||||
import eu.kanade.presentation.util.minimumTouchTargetSize
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
 | 
			
		||||
import tachiyomi.presentation.core.components.WheelDatePicker
 | 
			
		||||
import tachiyomi.presentation.core.components.WheelTextPicker
 | 
			
		||||
import tachiyomi.presentation.core.components.material.AlertDialogContent
 | 
			
		||||
import tachiyomi.presentation.core.components.material.Divider
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToStart
 | 
			
		||||
import tachiyomi.presentation.core.util.minimumTouchTargetSize
 | 
			
		||||
import java.time.LocalDate
 | 
			
		||||
import java.time.format.TextStyle
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TrackStatusSelector(
 | 
			
		||||
@@ -160,7 +161,7 @@ fun TrackDateSelector(
 | 
			
		||||
                        .weight(1f)
 | 
			
		||||
                        .padding(end = 16.dp),
 | 
			
		||||
                    text = internalSelection.dayOfWeek
 | 
			
		||||
                        .getDisplayName(TextStyle.SHORT, java.util.Locale.getDefault()),
 | 
			
		||||
                        .getDisplayName(TextStyle.SHORT, Locale.getDefault()),
 | 
			
		||||
                    textAlign = TextAlign.Center,
 | 
			
		||||
                    style = MaterialTheme.typography.titleMedium,
 | 
			
		||||
                )
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.manga
 | 
			
		||||
package eu.kanade.presentation.track
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
@@ -57,17 +57,17 @@ 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.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.util.runOnEnterKeyPressed
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
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.util.plus
 | 
			
		||||
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
 | 
			
		||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TrackServiceSearch(
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
package eu.kanade.presentation.track.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.Image
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
@@ -13,8 +13,8 @@ import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.util.clickableNoIndication
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import tachiyomi.presentation.core.util.clickableNoIndication
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TrackLogoIcon(
 | 
			
		||||
@@ -24,16 +24,16 @@ 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.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaBottomActionMenu
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -33,18 +33,18 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.font.FontStyle
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
 | 
			
		||||
import eu.kanade.presentation.components.ListGroupHeader
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
 | 
			
		||||
import eu.kanade.presentation.manga.components.DotSeparatorText
 | 
			
		||||
import eu.kanade.presentation.util.selectedBackground
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaCover
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
 | 
			
		||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
 | 
			
		||||
import tachiyomi.presentation.core.components.ListGroupHeader
 | 
			
		||||
import tachiyomi.presentation.core.components.material.ReadItemAlpha
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.selectedBackground
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import kotlin.time.Duration.Companion.minutes
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,125 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2021 The Android Open Source Project
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Straight copy from Compose M3 for Button fork
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.core.Animatable
 | 
			
		||||
import androidx.compose.animation.core.AnimationSpec
 | 
			
		||||
import androidx.compose.animation.core.CubicBezierEasing
 | 
			
		||||
import androidx.compose.animation.core.Easing
 | 
			
		||||
import androidx.compose.animation.core.FastOutSlowInEasing
 | 
			
		||||
import androidx.compose.animation.core.TweenSpec
 | 
			
		||||
import androidx.compose.foundation.interaction.DragInteraction
 | 
			
		||||
import androidx.compose.foundation.interaction.FocusInteraction
 | 
			
		||||
import androidx.compose.foundation.interaction.HoverInteraction
 | 
			
		||||
import androidx.compose.foundation.interaction.Interaction
 | 
			
		||||
import androidx.compose.foundation.interaction.PressInteraction
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Animates the [Dp] value of [this] between [from] and [to] [Interaction]s, to [target]. The
 | 
			
		||||
 * [AnimationSpec] used depends on the values for [from] and [to], see
 | 
			
		||||
 * [ElevationDefaults.incomingAnimationSpecForInteraction] and
 | 
			
		||||
 * [ElevationDefaults.outgoingAnimationSpecForInteraction] for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * @param target the [Dp] target elevation for this component, corresponding to the elevation
 | 
			
		||||
 * desired for the [to] state.
 | 
			
		||||
 * @param from the previous [Interaction] that was used to calculate elevation. `null` if there
 | 
			
		||||
 * was no previous [Interaction], such as when the component is in its default state.
 | 
			
		||||
 * @param to the [Interaction] that this component is moving to, such as [PressInteraction.Press]
 | 
			
		||||
 * when this component is being pressed. `null` if this component is moving back to its default
 | 
			
		||||
 * state.
 | 
			
		||||
 */
 | 
			
		||||
internal suspend fun Animatable<Dp, *>.animateElevation(
 | 
			
		||||
    target: Dp,
 | 
			
		||||
    from: Interaction? = null,
 | 
			
		||||
    to: Interaction? = null,
 | 
			
		||||
) {
 | 
			
		||||
    val spec = when {
 | 
			
		||||
        // Moving to a new state
 | 
			
		||||
        to != null -> ElevationDefaults.incomingAnimationSpecForInteraction(to)
 | 
			
		||||
        // Moving to default, from a previous state
 | 
			
		||||
        from != null -> ElevationDefaults.outgoingAnimationSpecForInteraction(from)
 | 
			
		||||
        // Loading the initial state, or moving back to the baseline state from a disabled /
 | 
			
		||||
        // unknown state, so just snap to the final value.
 | 
			
		||||
        else -> null
 | 
			
		||||
    }
 | 
			
		||||
    if (spec != null) animateTo(target, spec) else snapTo(target)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Contains default [AnimationSpec]s used for animating elevation between different [Interaction]s.
 | 
			
		||||
 *
 | 
			
		||||
 * Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s
 | 
			
		||||
 * internally. [animateElevation] in turn is used by the defaults for cards and buttons.
 | 
			
		||||
 *
 | 
			
		||||
 * @see animateElevation
 | 
			
		||||
 */
 | 
			
		||||
private object ElevationDefaults {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
 | 
			
		||||
     * previous [Interaction], or from the default state. If [interaction] is unknown, then
 | 
			
		||||
     * returns `null`.
 | 
			
		||||
     *
 | 
			
		||||
     * @param interaction the [Interaction] that is being animated to
 | 
			
		||||
     */
 | 
			
		||||
    fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
 | 
			
		||||
        return when (interaction) {
 | 
			
		||||
            is PressInteraction.Press -> DefaultIncomingSpec
 | 
			
		||||
            is DragInteraction.Start -> DefaultIncomingSpec
 | 
			
		||||
            is HoverInteraction.Enter -> DefaultIncomingSpec
 | 
			
		||||
            is FocusInteraction.Focus -> DefaultIncomingSpec
 | 
			
		||||
            else -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the [AnimationSpec]s used when animating elevation away from [interaction], to the
 | 
			
		||||
     * default state. If [interaction] is unknown, then returns `null`.
 | 
			
		||||
     *
 | 
			
		||||
     * @param interaction the [Interaction] that is being animated away from
 | 
			
		||||
     */
 | 
			
		||||
    fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
 | 
			
		||||
        return when (interaction) {
 | 
			
		||||
            is PressInteraction.Press -> DefaultOutgoingSpec
 | 
			
		||||
            is DragInteraction.Start -> DefaultOutgoingSpec
 | 
			
		||||
            is HoverInteraction.Enter -> HoveredOutgoingSpec
 | 
			
		||||
            is FocusInteraction.Focus -> DefaultOutgoingSpec
 | 
			
		||||
            else -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val OutgoingSpecEasing: Easing = CubicBezierEasing(0.40f, 0.00f, 0.60f, 1.00f)
 | 
			
		||||
 | 
			
		||||
private val DefaultIncomingSpec = TweenSpec<Dp>(
 | 
			
		||||
    durationMillis = 120,
 | 
			
		||||
    easing = FastOutSlowInEasing,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
private val DefaultOutgoingSpec = TweenSpec<Dp>(
 | 
			
		||||
    durationMillis = 150,
 | 
			
		||||
    easing = OutgoingSpecEasing,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
private val HoveredOutgoingSpec = TweenSpec<Dp>(
 | 
			
		||||
    durationMillis = 120,
 | 
			
		||||
    easing = OutgoingSpecEasing,
 | 
			
		||||
)
 | 
			
		||||
@@ -1,65 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.derivedStateOf
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyListState.isScrolledToStart(): Boolean {
 | 
			
		||||
    return remember {
 | 
			
		||||
        derivedStateOf {
 | 
			
		||||
            val firstItem = layoutInfo.visibleItemsInfo.firstOrNull()
 | 
			
		||||
            firstItem == null || firstItem.offset == layoutInfo.viewportStartOffset
 | 
			
		||||
        }
 | 
			
		||||
    }.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyListState.isScrolledToEnd(): Boolean {
 | 
			
		||||
    return remember {
 | 
			
		||||
        derivedStateOf {
 | 
			
		||||
            val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
 | 
			
		||||
            lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
 | 
			
		||||
        }
 | 
			
		||||
    }.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyListState.isScrollingUp(): Boolean {
 | 
			
		||||
    var previousIndex by remember { mutableStateOf(firstVisibleItemIndex) }
 | 
			
		||||
    var previousScrollOffset by remember { mutableStateOf(firstVisibleItemScrollOffset) }
 | 
			
		||||
    return remember {
 | 
			
		||||
        derivedStateOf {
 | 
			
		||||
            if (previousIndex != firstVisibleItemIndex) {
 | 
			
		||||
                previousIndex > firstVisibleItemIndex
 | 
			
		||||
            } else {
 | 
			
		||||
                previousScrollOffset >= firstVisibleItemScrollOffset
 | 
			
		||||
            }.also {
 | 
			
		||||
                previousIndex = firstVisibleItemIndex
 | 
			
		||||
                previousScrollOffset = firstVisibleItemScrollOffset
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyListState.isScrollingDown(): Boolean {
 | 
			
		||||
    var previousIndex by remember { mutableStateOf(firstVisibleItemIndex) }
 | 
			
		||||
    var previousScrollOffset by remember { mutableStateOf(firstVisibleItemScrollOffset) }
 | 
			
		||||
    return remember {
 | 
			
		||||
        derivedStateOf {
 | 
			
		||||
            if (previousIndex != firstVisibleItemIndex) {
 | 
			
		||||
                previousIndex < firstVisibleItemIndex
 | 
			
		||||
            } else {
 | 
			
		||||
                previousScrollOffset <= firstVisibleItemScrollOffset
 | 
			
		||||
            }.also {
 | 
			
		||||
                previousIndex = firstVisibleItemIndex
 | 
			
		||||
                previousScrollOffset = firstVisibleItemScrollOffset
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }.value
 | 
			
		||||
}
 | 
			
		||||
@@ -1,108 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.interaction.MutableInteractionSource
 | 
			
		||||
import androidx.compose.foundation.isSystemInDarkTheme
 | 
			
		||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.composed
 | 
			
		||||
import androidx.compose.ui.draw.alpha
 | 
			
		||||
import androidx.compose.ui.input.key.Key
 | 
			
		||||
import androidx.compose.ui.input.key.key
 | 
			
		||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
 | 
			
		||||
import androidx.compose.ui.layout.LayoutModifier
 | 
			
		||||
import androidx.compose.ui.layout.Measurable
 | 
			
		||||
import androidx.compose.ui.layout.MeasureResult
 | 
			
		||||
import androidx.compose.ui.layout.MeasureScope
 | 
			
		||||
import androidx.compose.ui.platform.LocalViewConfiguration
 | 
			
		||||
import androidx.compose.ui.platform.debugInspectorInfo
 | 
			
		||||
import androidx.compose.ui.unit.Constraints
 | 
			
		||||
import androidx.compose.ui.unit.DpSize
 | 
			
		||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed {
 | 
			
		||||
    if (isSelected) {
 | 
			
		||||
        val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f
 | 
			
		||||
        background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
 | 
			
		||||
    } else {
 | 
			
		||||
        this
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha)
 | 
			
		||||
 | 
			
		||||
fun Modifier.clickableNoIndication(
 | 
			
		||||
    onLongClick: (() -> Unit)? = null,
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
): Modifier = composed {
 | 
			
		||||
    this.combinedClickable(
 | 
			
		||||
        interactionSource = remember { MutableInteractionSource() },
 | 
			
		||||
        indication = null,
 | 
			
		||||
        onLongClick = onLongClick,
 | 
			
		||||
        onClick = onClick,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For TextField, the provided [action] will be invoked when
 | 
			
		||||
 * physical enter key is pressed.
 | 
			
		||||
 *
 | 
			
		||||
 * Naturally, the TextField should be set to single line only.
 | 
			
		||||
 */
 | 
			
		||||
fun Modifier.runOnEnterKeyPressed(action: () -> Unit): Modifier = this.onPreviewKeyEvent {
 | 
			
		||||
    when (it.key) {
 | 
			
		||||
        Key.Enter, Key.NumPadEnter -> {
 | 
			
		||||
            action()
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
        else -> false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Suppress("ModifierInspectorInfo")
 | 
			
		||||
fun Modifier.minimumTouchTargetSize(): Modifier = composed(
 | 
			
		||||
    inspectorInfo = debugInspectorInfo {
 | 
			
		||||
        name = "minimumTouchTargetSize"
 | 
			
		||||
        properties["README"] = "Adds outer padding to measure at least 48.dp (default) in " +
 | 
			
		||||
            "size to disambiguate touch interactions if the element would measure smaller"
 | 
			
		||||
    },
 | 
			
		||||
) {
 | 
			
		||||
    if (LocalMinimumTouchTargetEnforcement.current) {
 | 
			
		||||
        val size = LocalViewConfiguration.current.minimumTouchTargetSize
 | 
			
		||||
        MinimumTouchTargetModifier(size)
 | 
			
		||||
    } else {
 | 
			
		||||
        Modifier
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private class MinimumTouchTargetModifier(val size: DpSize) : LayoutModifier {
 | 
			
		||||
    override fun MeasureScope.measure(
 | 
			
		||||
        measurable: Measurable,
 | 
			
		||||
        constraints: Constraints,
 | 
			
		||||
    ): MeasureResult {
 | 
			
		||||
        val placeable = measurable.measure(constraints)
 | 
			
		||||
 | 
			
		||||
        // Be at least as big as the minimum dimension in both dimensions
 | 
			
		||||
        val width = maxOf(placeable.width, size.width.roundToPx())
 | 
			
		||||
        val height = maxOf(placeable.height, size.height.roundToPx())
 | 
			
		||||
 | 
			
		||||
        return layout(width, height) {
 | 
			
		||||
            val centerX = ((width - placeable.width) / 2f).roundToInt()
 | 
			
		||||
            val centerY = ((height - placeable.height) / 2f).roundToInt()
 | 
			
		||||
            placeable.place(centerX, centerY)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        val otherModifier = other as? MinimumTouchTargetModifier ?: return false
 | 
			
		||||
        return size == otherModifier.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return size.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.calculateEndPadding
 | 
			
		||||
import androidx.compose.foundation.layout.calculateStartPadding
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.ReadOnlyComposable
 | 
			
		||||
import androidx.compose.ui.platform.LocalLayoutDirection
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@ReadOnlyComposable
 | 
			
		||||
operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
 | 
			
		||||
    val layoutDirection = LocalLayoutDirection.current
 | 
			
		||||
    return PaddingValues(
 | 
			
		||||
        start = calculateStartPadding(layoutDirection) +
 | 
			
		||||
            other.calculateStartPadding(layoutDirection),
 | 
			
		||||
        end = calculateEndPadding(layoutDirection) +
 | 
			
		||||
            other.calculateEndPadding(layoutDirection),
 | 
			
		||||
        top = calculateTopPadding() + other.calculateTopPadding(),
 | 
			
		||||
        bottom = calculateBottomPadding() + other.calculateBottomPadding(),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
 | 
			
		||||
@Preview(
 | 
			
		||||
    name = "Light",
 | 
			
		||||
    showBackground = true,
 | 
			
		||||
)
 | 
			
		||||
@Preview(
 | 
			
		||||
    name = "Dark",
 | 
			
		||||
    showBackground = true,
 | 
			
		||||
    uiMode = Configuration.UI_MODE_NIGHT_YES,
 | 
			
		||||
)
 | 
			
		||||
annotation class ThemePreviews
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.core.AnimationState
 | 
			
		||||
import androidx.compose.animation.core.DecayAnimationSpec
 | 
			
		||||
import androidx.compose.animation.core.animateDecay
 | 
			
		||||
import androidx.compose.animation.rememberSplineBasedDecay
 | 
			
		||||
import androidx.compose.foundation.gestures.FlingBehavior
 | 
			
		||||
import androidx.compose.foundation.gestures.ScrollScope
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.MotionDurationScale
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * FlingBehavior that always uses the default motion scale.
 | 
			
		||||
 *
 | 
			
		||||
 * This makes the scrolling animation works like View's lists
 | 
			
		||||
 * when "Remove animation" settings is on.
 | 
			
		||||
 */
 | 
			
		||||
@Composable
 | 
			
		||||
fun flingBehaviorIgnoringMotionScale(): FlingBehavior {
 | 
			
		||||
    val flingSpec = rememberSplineBasedDecay<Float>()
 | 
			
		||||
    return remember(flingSpec) {
 | 
			
		||||
        DefaultFlingBehavior(flingSpec)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val DefaultMotionDurationScale = object : MotionDurationScale {
 | 
			
		||||
    // Use default motion scale factor
 | 
			
		||||
    override val scaleFactor: Float = 1f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private class DefaultFlingBehavior(
 | 
			
		||||
    private val flingDecay: DecayAnimationSpec<Float>,
 | 
			
		||||
) : FlingBehavior {
 | 
			
		||||
    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
 | 
			
		||||
        // come up with the better threshold, but we need it since spline curve gives us NaNs
 | 
			
		||||
        return if (abs(initialVelocity) > 1f) {
 | 
			
		||||
            var velocityLeft = initialVelocity
 | 
			
		||||
            var lastValue = 0f
 | 
			
		||||
            withContext(DefaultMotionDurationScale) {
 | 
			
		||||
                AnimationState(
 | 
			
		||||
                    initialValue = 0f,
 | 
			
		||||
                    initialVelocity = initialVelocity,
 | 
			
		||||
                ).animateDecay(flingDecay) {
 | 
			
		||||
                    val delta = value - lastValue
 | 
			
		||||
                    val consumed = scrollBy(delta)
 | 
			
		||||
                    lastValue = value
 | 
			
		||||
                    velocityLeft = this.velocity
 | 
			
		||||
                    // avoid rounding errors and stop if anything is unconsumed
 | 
			
		||||
                    if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            velocityLeft
 | 
			
		||||
        } else {
 | 
			
		||||
            initialVelocity
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,273 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.util
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * MIT License
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (c) 2022 Albert Chang
 | 
			
		||||
 *
 | 
			
		||||
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
 * of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
 * in the Software without restriction, including without limitation the rights
 | 
			
		||||
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
 * copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
 * furnished to do so, subject to the following conditions:
 | 
			
		||||
 *
 | 
			
		||||
 * The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
 * copies or substantial portions of the Software.
 | 
			
		||||
 *
 | 
			
		||||
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
 * SOFTWARE.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Code taken from https://gist.github.com/mxalbert1996/33a360fcab2105a31e5355af98216f5a
 | 
			
		||||
 * with some modifications to handle contentPadding.
 | 
			
		||||
 *
 | 
			
		||||
 * Modifiers for regular scrollable list is omitted.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import android.view.ViewConfiguration
 | 
			
		||||
import androidx.compose.animation.core.Animatable
 | 
			
		||||
import androidx.compose.animation.core.tween
 | 
			
		||||
import androidx.compose.foundation.gestures.Orientation
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyRow
 | 
			
		||||
import androidx.compose.foundation.lazy.rememberLazyListState
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.composed
 | 
			
		||||
import androidx.compose.ui.draw.drawWithContent
 | 
			
		||||
import androidx.compose.ui.geometry.Offset
 | 
			
		||||
import androidx.compose.ui.geometry.Size
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 | 
			
		||||
import androidx.compose.ui.graphics.drawscope.DrawScope
 | 
			
		||||
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.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalLayoutDirection
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.LayoutDirection
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.util.fastFirstOrNull
 | 
			
		||||
import androidx.compose.ui.util.fastSumBy
 | 
			
		||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
 | 
			
		||||
import kotlinx.coroutines.channels.BufferOverflow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws horizontal scrollbar to a LazyList.
 | 
			
		||||
 *
 | 
			
		||||
 * Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
 | 
			
		||||
 */
 | 
			
		||||
fun Modifier.drawHorizontalScrollbar(
 | 
			
		||||
    state: LazyListState,
 | 
			
		||||
    reverseScrolling: Boolean = false,
 | 
			
		||||
    // The amount of offset the scrollbar position towards the top of the layout
 | 
			
		||||
    positionOffsetPx: Float = 0f,
 | 
			
		||||
): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling, positionOffsetPx)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws vertical scrollbar to a LazyList.
 | 
			
		||||
 *
 | 
			
		||||
 * Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
 | 
			
		||||
 */
 | 
			
		||||
fun Modifier.drawVerticalScrollbar(
 | 
			
		||||
    state: LazyListState,
 | 
			
		||||
    reverseScrolling: Boolean = false,
 | 
			
		||||
    // The amount of offset the scrollbar position towards the start of the layout
 | 
			
		||||
    positionOffsetPx: Float = 0f,
 | 
			
		||||
): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling, positionOffsetPx)
 | 
			
		||||
 | 
			
		||||
private fun Modifier.drawScrollbar(
 | 
			
		||||
    state: LazyListState,
 | 
			
		||||
    orientation: Orientation,
 | 
			
		||||
    reverseScrolling: Boolean,
 | 
			
		||||
    positionOffset: Float,
 | 
			
		||||
): Modifier = drawScrollbar(
 | 
			
		||||
    orientation,
 | 
			
		||||
    reverseScrolling,
 | 
			
		||||
) { reverseDirection, atEnd, thickness, color, alpha ->
 | 
			
		||||
    val layoutInfo = state.layoutInfo
 | 
			
		||||
    val viewportSize = if (orientation == Orientation.Horizontal) {
 | 
			
		||||
        layoutInfo.viewportSize.width
 | 
			
		||||
    } else {
 | 
			
		||||
        layoutInfo.viewportSize.height
 | 
			
		||||
    } - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding
 | 
			
		||||
    val items = layoutInfo.visibleItemsInfo
 | 
			
		||||
    val itemsSize = items.fastSumBy { it.size }
 | 
			
		||||
    val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize
 | 
			
		||||
    val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size
 | 
			
		||||
    val totalSize = estimatedItemSize * layoutInfo.totalItemsCount
 | 
			
		||||
    val thumbSize = viewportSize / totalSize * viewportSize
 | 
			
		||||
    val startOffset = if (items.isEmpty()) {
 | 
			
		||||
        0f
 | 
			
		||||
    } else {
 | 
			
		||||
        items
 | 
			
		||||
            .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
 | 
			
		||||
            .run {
 | 
			
		||||
                val startPadding = if (reverseDirection) layoutInfo.afterContentPadding else layoutInfo.beforeContentPadding
 | 
			
		||||
                startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
    val drawScrollbar = onDrawScrollbar(
 | 
			
		||||
        orientation, reverseDirection, atEnd, showScrollbar,
 | 
			
		||||
        thickness, color, alpha, thumbSize, startOffset, positionOffset,
 | 
			
		||||
    )
 | 
			
		||||
    drawContent()
 | 
			
		||||
    drawScrollbar()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun ContentDrawScope.onDrawScrollbar(
 | 
			
		||||
    orientation: Orientation,
 | 
			
		||||
    reverseDirection: Boolean,
 | 
			
		||||
    atEnd: Boolean,
 | 
			
		||||
    showScrollbar: Boolean,
 | 
			
		||||
    thickness: Float,
 | 
			
		||||
    color: Color,
 | 
			
		||||
    alpha: () -> Float,
 | 
			
		||||
    thumbSize: Float,
 | 
			
		||||
    scrollOffset: Float,
 | 
			
		||||
    positionOffset: Float,
 | 
			
		||||
): DrawScope.() -> Unit {
 | 
			
		||||
    val topLeft = if (orientation == Orientation.Horizontal) {
 | 
			
		||||
        Offset(
 | 
			
		||||
            if (reverseDirection) size.width - scrollOffset - thumbSize else scrollOffset,
 | 
			
		||||
            if (atEnd) size.height - positionOffset - thickness else positionOffset,
 | 
			
		||||
        )
 | 
			
		||||
    } else {
 | 
			
		||||
        Offset(
 | 
			
		||||
            if (atEnd) size.width - positionOffset - thickness else positionOffset,
 | 
			
		||||
            if (reverseDirection) size.height - scrollOffset - thumbSize else scrollOffset,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    val size = if (orientation == Orientation.Horizontal) {
 | 
			
		||||
        Size(thumbSize, thickness)
 | 
			
		||||
    } else {
 | 
			
		||||
        Size(thickness, thumbSize)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        if (showScrollbar) {
 | 
			
		||||
            drawRect(
 | 
			
		||||
                color = color,
 | 
			
		||||
                topLeft = topLeft,
 | 
			
		||||
                size = size,
 | 
			
		||||
                alpha = alpha(),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun Modifier.drawScrollbar(
 | 
			
		||||
    orientation: Orientation,
 | 
			
		||||
    reverseScrolling: Boolean,
 | 
			
		||||
    onDraw: ContentDrawScope.(
 | 
			
		||||
        reverseDirection: Boolean,
 | 
			
		||||
        atEnd: Boolean,
 | 
			
		||||
        thickness: Float,
 | 
			
		||||
        color: Color,
 | 
			
		||||
        alpha: () -> Float,
 | 
			
		||||
    ) -> Unit,
 | 
			
		||||
): Modifier = composed {
 | 
			
		||||
    val scrolled = remember {
 | 
			
		||||
        MutableSharedFlow<Unit>(
 | 
			
		||||
            extraBufferCapacity = 1,
 | 
			
		||||
            onBufferOverflow = BufferOverflow.DROP_OLDEST,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    val nestedScrollConnection = remember(orientation, scrolled) {
 | 
			
		||||
        object : NestedScrollConnection {
 | 
			
		||||
            override fun onPostScroll(
 | 
			
		||||
                consumed: Offset,
 | 
			
		||||
                available: Offset,
 | 
			
		||||
                source: NestedScrollSource,
 | 
			
		||||
            ): Offset {
 | 
			
		||||
                val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y
 | 
			
		||||
                if (delta != 0f) scrolled.tryEmit(Unit)
 | 
			
		||||
                return Offset.Zero
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val alpha = remember { Animatable(0f) }
 | 
			
		||||
    LaunchedEffect(scrolled, alpha) {
 | 
			
		||||
        scrolled.collectLatest {
 | 
			
		||||
            alpha.snapTo(1f)
 | 
			
		||||
            alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
 | 
			
		||||
    val reverseDirection = if (orientation == Orientation.Horizontal) {
 | 
			
		||||
        if (isLtr) reverseScrolling else !reverseScrolling
 | 
			
		||||
    } else {
 | 
			
		||||
        reverseScrolling
 | 
			
		||||
    }
 | 
			
		||||
    val atEnd = if (orientation == Orientation.Vertical) isLtr else true
 | 
			
		||||
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
    val thickness = remember { ViewConfiguration.get(context).scaledScrollBarSize.toFloat() }
 | 
			
		||||
    val color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.364f)
 | 
			
		||||
    Modifier
 | 
			
		||||
        .nestedScroll(nestedScrollConnection)
 | 
			
		||||
        .drawWithContent {
 | 
			
		||||
            onDraw(reverseDirection, atEnd, thickness, color, alpha::value)
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val FadeOutAnimationSpec = tween<Float>(
 | 
			
		||||
    durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
 | 
			
		||||
    delayMillis = ViewConfiguration.getScrollDefaultDelay(),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Preview(widthDp = 400, heightDp = 400, showBackground = true)
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyListScrollbarPreview() {
 | 
			
		||||
    val state = rememberLazyListState()
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        modifier = Modifier.drawVerticalScrollbar(state),
 | 
			
		||||
        state = state,
 | 
			
		||||
    ) {
 | 
			
		||||
        items(50) {
 | 
			
		||||
            Text(
 | 
			
		||||
                text = "Item ${it + 1}",
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .padding(16.dp),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Preview(widthDp = 400, showBackground = true)
 | 
			
		||||
@Composable
 | 
			
		||||
fun LazyListHorizontalScrollbarPreview() {
 | 
			
		||||
    val state = rememberLazyListState()
 | 
			
		||||
    LazyRow(
 | 
			
		||||
        modifier = Modifier.drawHorizontalScrollbar(state),
 | 
			
		||||
        state = state,
 | 
			
		||||
    ) {
 | 
			
		||||
        items(50) {
 | 
			
		||||
            Text(
 | 
			
		||||
                text = (it + 1).toString(),
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(horizontal = 8.dp, vertical = 16.dp),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -42,8 +42,8 @@ import eu.kanade.presentation.browse.BrowseSourceContent
 | 
			
		||||
import eu.kanade.presentation.browse.MissingSourceScreen
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
 | 
			
		||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.dialogs.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.dialogs.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.util.AssistContentScreen
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
 
 | 
			
		||||
@@ -29,13 +29,13 @@ import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
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.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DeleteLibraryMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreenAction
 | 
			
		||||
import eu.kanade.presentation.components.LibraryBottomActionMenu
 | 
			
		||||
import eu.kanade.presentation.components.dialogs.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.dialogs.DeleteLibraryMangaDialog
 | 
			
		||||
import eu.kanade.presentation.library.components.LibraryContent
 | 
			
		||||
import eu.kanade.presentation.library.components.LibraryToolbar
 | 
			
		||||
import eu.kanade.presentation.manga.components.LibraryBottomActionMenu
 | 
			
		||||
import eu.kanade.presentation.util.Tab
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,9 @@ import cafe.adriel.voyager.navigator.Navigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.domain.manga.model.hasCustomCover
 | 
			
		||||
import eu.kanade.domain.manga.model.toSManga
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
 | 
			
		||||
import eu.kanade.presentation.components.dialogs.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.dialogs.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.manga.ChapterSettingsDialog
 | 
			
		||||
import eu.kanade.presentation.manga.EditCoverAction
 | 
			
		||||
import eu.kanade.presentation.manga.MangaScreen
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ import eu.kanade.domain.manga.model.isLocal
 | 
			
		||||
import eu.kanade.domain.manga.model.toSManga
 | 
			
		||||
import eu.kanade.domain.track.model.toDbTrack
 | 
			
		||||
import eu.kanade.domain.ui.UiPreferences
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.DownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
 
 | 
			
		||||
@@ -40,12 +40,12 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
 | 
			
		||||
import eu.kanade.domain.track.model.toDbTrack
 | 
			
		||||
import eu.kanade.domain.track.model.toDomainTrack
 | 
			
		||||
import eu.kanade.domain.ui.UiPreferences
 | 
			
		||||
import eu.kanade.presentation.manga.TrackChapterSelector
 | 
			
		||||
import eu.kanade.presentation.manga.TrackDateSelector
 | 
			
		||||
import eu.kanade.presentation.manga.TrackInfoDialogHome
 | 
			
		||||
import eu.kanade.presentation.manga.TrackScoreSelector
 | 
			
		||||
import eu.kanade.presentation.manga.TrackServiceSearch
 | 
			
		||||
import eu.kanade.presentation.manga.TrackStatusSelector
 | 
			
		||||
import eu.kanade.presentation.track.TrackChapterSelector
 | 
			
		||||
import eu.kanade.presentation.track.TrackDateSelector
 | 
			
		||||
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.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ import eu.kanade.core.util.insertSeparators
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
 | 
			
		||||
import eu.kanade.domain.library.service.LibraryPreferences
 | 
			
		||||
import eu.kanade.domain.ui.UiPreferences
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.updates.UpdatesUiModel
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user