Move more components to presentation-core module

This commit is contained in:
arkon
2023-02-18 16:33:03 -05:00
parent bfe143015a
commit 58a0add4f6
92 changed files with 176 additions and 175 deletions

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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(

View File

@ -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,
)
}
}

View File

@ -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,
)
}
}

View File

@ -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,
)
}

View File

@ -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(

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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(

View File

@ -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,
)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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))

View File

@ -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

View File

@ -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(

View File

@ -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
/**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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(

View File

@ -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) {

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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(),
)
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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),
)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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