fix(sticky-header): Fix sticky-header and scrolling issues in source screens (#1315)

* Fix padding in MigrateSourceScreen

* Fix scrolling bar glitch due to sticky headers in RelatedMangasList

* Dynamically measure source search's height

* sticky header in source screen

* fix(sticky-header): update sticky header keys for improved uniqueness

* Fix padding on related-manga-grid

* Avoid mixing dp and px units when initializing searchBoxHeight

* extract `hasKeyword`

* fillMaxWidth
This commit is contained in:
Cuong-Tran 2025-12-09 13:19:54 +07:00 committed by GitHub
parent b24b14c73d
commit b648afb889
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 296 additions and 200 deletions

View file

@ -30,13 +30,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.installedExtension import eu.kanade.domain.source.model.installedExtension
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.components.AnimatedFloatingSearchBox import eu.kanade.presentation.components.AnimatedFloatingSearchBox
import eu.kanade.presentation.components.SOURCE_SEARCH_BOX_HEIGHT
import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
@ -180,9 +181,12 @@ private fun MigrateSourceList(
// KMK --> // KMK -->
Box { Box {
val density = LocalDensity.current
var searchBoxHeight by remember { mutableStateOf(SOURCE_SEARCH_BOX_HEIGHT) }
FastScrollLazyColumn( FastScrollLazyColumn(
state = lazyListState, state = lazyListState,
contentPadding = PaddingValues(top = 65.dp), contentPadding = PaddingValues(top = searchBoxHeight),
// KMK <-- // KMK <--
) { ) {
items( items(
@ -201,7 +205,8 @@ private fun MigrateSourceList(
MigrateSourceItem( MigrateSourceItem(
// KMK --> // KMK -->
// modifier = Modifier.animateItem(), // modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(), modifier = Modifier.animateItemFastScroll()
.padding(end = MaterialTheme.padding.small),
// KMK <-- // KMK <--
source = source, source = source,
count = count, count = count,
@ -221,9 +226,11 @@ private fun MigrateSourceList(
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding( .padding(
horizontal = MaterialTheme.padding.medium, horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
) )
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
onGloballyPositioned = { layoutCoordinates ->
searchBoxHeight = with(density) { layoutCoordinates.size.height.toDp() }
},
) )
// KMK <-- // KMK <--
} }

View file

@ -86,7 +86,7 @@ private fun SourcesFilterContent(
// KMK --> // KMK -->
stickyHeader( stickyHeader(
// KMK <-- // KMK <--
key = language, key = "$STICKY_HEADER_KEY_PREFIX-$language",
contentType = "source-filter-header", contentType = "source-filter-header",
) { ) {
SourcesFilterHeader( SourcesFilterHeader(

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -23,15 +22,21 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import eu.kanade.domain.source.model.installedExtension import eu.kanade.domain.source.model.installedExtension
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AnimatedFloatingSearchBox import eu.kanade.presentation.components.AnimatedFloatingSearchBox
import eu.kanade.presentation.components.SOURCE_SEARCH_BOX_HEIGHT
import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
@ -46,6 +51,7 @@ import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -88,47 +94,51 @@ fun SourcesScreen(
else -> Box( else -> Box(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) { ) {
val density = LocalDensity.current
var searchBoxHeight by remember { mutableStateOf(SOURCE_SEARCH_BOX_HEIGHT) }
FastScrollLazyColumn( FastScrollLazyColumn(
state = lazyListState, state = lazyListState,
contentPadding = PaddingValues(top = 65.dp), contentPadding = PaddingValues(top = searchBoxHeight),
// KMK <-- // KMK <--
) { ) {
items( state.items.forEach { model ->
items = state.items,
contentType = {
when (it) {
is SourceUiModel.Header -> "header"
is SourceUiModel.Item -> "item"
}
},
key = {
when (it) {
is SourceUiModel.Header -> "header-${it.hashCode()}"
is SourceUiModel.Item -> "source-${it.source.key()}"
}
},
) { model ->
when (model) { when (model) {
is SourceUiModel.Header -> { is SourceUiModel.Header -> {
SourceHeader( stickyHeader(
modifier = Modifier.animateItemFastScroll(), key = "$STICKY_HEADER_KEY_PREFIX-header-${model.hashCode()}",
language = model.language, contentType = "header",
// SY --> ) {
isCategory = model.isCategory, SourceHeader(
// SY <-- modifier = Modifier
) .animateItemFastScroll()
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
language = model.language,
// SY -->
isCategory = model.isCategory,
// SY <--
)
}
}
is SourceUiModel.Item -> {
item(
key = "source-${model.source.key()}",
contentType = "item",
) {
SourceItem(
modifier = Modifier.animateItemFastScroll(),
source = model.source,
// SY -->
showLatest = state.showLatest,
showPin = state.showPin,
// SY <--
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onClickPin = onClickPin,
)
}
} }
is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemFastScroll(),
source = model.source,
// SY -->
showLatest = state.showLatest,
showPin = state.showPin,
// SY <--
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onClickPin = onClickPin,
)
} }
} }
} }
@ -146,6 +156,9 @@ fun SourcesScreen(
vertical = MaterialTheme.padding.small, vertical = MaterialTheme.padding.small,
) )
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
onGloballyPositioned = { layoutCoordinates ->
searchBoxHeight = with(density) { layoutCoordinates.size.height.toDp() + 2 * MaterialTheme.padding.small }
},
) )
// KMK <-- // KMK <--
} }

View file

@ -2,7 +2,10 @@ package eu.kanade.presentation.browse.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -39,59 +42,81 @@ fun RelatedMangasComfortableGrid(
) { ) {
FastScrollLazyVerticalGrid( FastScrollLazyVerticalGrid(
columns = columns, columns = columns,
contentPadding = contentPadding + PaddingValues(horizontal = MaterialTheme.padding.small), contentPadding = contentPadding + PaddingValues(horizontal = MaterialTheme.padding.small), // padding for scrollbar
// padding for scrollbar
topContentPadding = contentPadding.calculateTopPadding(), topContentPadding = contentPadding.calculateTopPadding(),
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
) { ) {
relatedMangas.forEach { related -> relatedMangas.forEach { relatedManga ->
val isLoading = related is RelatedManga.Loading when (relatedManga) {
if (isLoading) { is RelatedManga.Loading -> {
header(key = "${related.hashCode()}#header") { header(key = "${relatedManga.hashCode()}#header") {
RelatedMangaTitle( Column(
title = stringResource(MR.strings.loading), modifier = Modifier
subtitle = null, .fillMaxWidth()
onClick = {}, .background(MaterialTheme.colorScheme.background),
onLongClick = null, ) {
modifier = Modifier.background(MaterialTheme.colorScheme.background), HorizontalDivider()
) RelatedMangaTitle(
title = stringResource(MR.strings.loading),
subtitle = null,
onClick = {},
onLongClick = null,
modifier = Modifier
.padding(
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.small,
),
)
}
}
header(key = "${relatedManga.hashCode()}#loading") { RelatedMangasLoadingItem() }
} }
header(key = "${related.hashCode()}#content") { RelatedMangasLoadingItem() } is RelatedManga.Success -> {
} else { val hasKeyword = relatedManga.keyword.isNotBlank()
val relatedManga = related as RelatedManga.Success header(key = "${relatedManga.hashCode()}#header") {
header(key = "${related.hashCode()}#divider") { HorizontalDivider() } Column(
header(key = "${related.hashCode()}#header") { modifier = Modifier
RelatedMangaTitle( .fillMaxWidth()
title = if (relatedManga.keyword.isNotBlank()) { .background(MaterialTheme.colorScheme.background),
stringResource(KMR.strings.related_mangas_more) ) {
} else { HorizontalDivider()
stringResource(KMR.strings.related_mangas_website_suggestions) RelatedMangaTitle(
}, title = if (hasKeyword) {
showArrow = relatedManga.keyword.isNotBlank(), stringResource(KMR.strings.related_mangas_more)
subtitle = null, } else {
onClick = { stringResource(KMR.strings.related_mangas_website_suggestions)
if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword) },
}, showArrow = hasKeyword,
onLongClick = { subtitle = null,
if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword) onClick = {
}, if (hasKeyword) onKeywordClick(relatedManga.keyword)
modifier = Modifier.background(MaterialTheme.colorScheme.background), },
) onLongClick = {
} if (hasKeyword) onKeywordLongClick(relatedManga.keyword)
items( },
key = { "related-comfort-${relatedManga.mangaList[it].id}" }, modifier = Modifier
count = relatedManga.mangaList.size, .padding(
) { index -> start = MaterialTheme.padding.small,
val manga by getManga(relatedManga.mangaList[index]) end = MaterialTheme.padding.small,
BrowseSourceComfortableGridItem( ),
manga = manga, )
onClick = { onMangaClick(manga) }, }
onLongClick = { onMangaLongClick(manga) }, }
isSelected = selection.fastAny { selected -> selected.id == manga.id }, items(
metadata = null, key = { "related-comfort-${relatedManga.mangaList[it].id}" },
usePanoramaCover = usePanoramaCover, count = relatedManga.mangaList.size,
) ) { index ->
val manga by getManga(relatedManga.mangaList[index])
BrowseSourceComfortableGridItem(
manga = manga,
onClick = { onMangaClick(manga) },
onLongClick = { onMangaLongClick(manga) },
isSelected = selection.fastAny { selected -> selected.id == manga.id },
metadata = null,
usePanoramaCover = usePanoramaCover,
)
}
} }
} }
} }

View file

@ -2,7 +2,10 @@ package eu.kanade.presentation.browse.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -38,58 +41,80 @@ fun RelatedMangasCompactGrid(
) { ) {
FastScrollLazyVerticalGrid( FastScrollLazyVerticalGrid(
columns = columns, columns = columns,
contentPadding = contentPadding + PaddingValues(horizontal = MaterialTheme.padding.small), contentPadding = contentPadding + PaddingValues(horizontal = MaterialTheme.padding.small), // padding for scrollbar
// padding for scrollbar
topContentPadding = contentPadding.calculateTopPadding(), topContentPadding = contentPadding.calculateTopPadding(),
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
) { ) {
relatedMangas.forEach { related -> relatedMangas.forEach { relatedManga ->
val isLoading = related is RelatedManga.Loading when (relatedManga) {
if (isLoading) { is RelatedManga.Loading -> {
header(key = "${related.hashCode()}#header") { header(key = "${relatedManga.hashCode()}#header") {
RelatedMangaTitle( Column(
title = stringResource(MR.strings.loading), modifier = Modifier
subtitle = null, .fillMaxWidth()
onClick = {}, .background(MaterialTheme.colorScheme.background),
onLongClick = null, ) {
modifier = Modifier.background(MaterialTheme.colorScheme.background), HorizontalDivider()
) RelatedMangaTitle(
title = stringResource(MR.strings.loading),
subtitle = null,
onClick = {},
onLongClick = null,
modifier = Modifier
.padding(
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.small,
),
)
}
}
header(key = "${relatedManga.hashCode()}#loading") { RelatedMangasLoadingItem() }
} }
header(key = "${related.hashCode()}#content") { RelatedMangasLoadingItem() } is RelatedManga.Success -> {
} else { val hasKeyword = relatedManga.keyword.isNotBlank()
val relatedManga = related as RelatedManga.Success header(key = "${relatedManga.hashCode()}#header") {
header(key = "${related.hashCode()}#divider") { HorizontalDivider() } Column(
header(key = "${related.hashCode()}#header") { modifier = Modifier
RelatedMangaTitle( .fillMaxWidth()
title = if (relatedManga.keyword.isNotBlank()) { .background(MaterialTheme.colorScheme.background),
stringResource(KMR.strings.related_mangas_more) ) {
} else { HorizontalDivider()
stringResource(KMR.strings.related_mangas_website_suggestions) RelatedMangaTitle(
}, title = if (hasKeyword) {
showArrow = relatedManga.keyword.isNotBlank(), stringResource(KMR.strings.related_mangas_more)
subtitle = null, } else {
onClick = { stringResource(KMR.strings.related_mangas_website_suggestions)
if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword) },
}, showArrow = hasKeyword,
onLongClick = { subtitle = null,
if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword) onClick = {
}, if (hasKeyword) onKeywordClick(relatedManga.keyword)
modifier = Modifier.background(MaterialTheme.colorScheme.background), },
) onLongClick = {
} if (hasKeyword) onKeywordLongClick(relatedManga.keyword)
items( },
key = { "related-compact-${relatedManga.mangaList[it].id}" }, modifier = Modifier
count = relatedManga.mangaList.size, .padding(
) { index -> start = MaterialTheme.padding.small,
val manga by getManga(relatedManga.mangaList[index]) end = MaterialTheme.padding.small,
BrowseSourceCompactGridItem( ),
manga = manga, )
onClick = { onMangaClick(manga) }, }
onLongClick = { onMangaLongClick(manga) }, }
isSelected = selection.fastAny { selected -> selected.id == manga.id }, items(
metadata = null, key = { "related-compact-${relatedManga.mangaList[it].id}" },
) count = relatedManga.mangaList.size,
) { index ->
val manga by getManga(relatedManga.mangaList[index])
BrowseSourceCompactGridItem(
manga = manga,
onClick = { onMangaClick(manga) },
onLongClick = { onMangaLongClick(manga) },
isSelected = selection.fastAny { selected -> selected.id == manga.id },
metadata = null,
)
}
} }
} }
} }

View file

@ -1,7 +1,9 @@
package eu.kanade.presentation.browse.components package eu.kanade.presentation.browse.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -36,63 +38,75 @@ fun RelatedMangasList(
// Using modifier instead of contentPadding so we can use stickyHeader // Using modifier instead of contentPadding so we can use stickyHeader
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) { ) {
relatedMangas.forEach { related -> relatedMangas.forEach { relatedManga ->
val isLoading = related is RelatedManga.Loading when (relatedManga) {
if (isLoading) { is RelatedManga.Loading -> {
item(key = "${related.hashCode()}#divider") { HorizontalDivider() } stickyHeader(key = "$STICKY_HEADER_KEY_PREFIX-${relatedManga.hashCode()}#header") {
stickyHeader(key = "$STICKY_HEADER_KEY_PREFIX${related.hashCode()}#header") { Column(
RelatedMangaTitle( modifier = Modifier
title = stringResource(MR.strings.loading), .fillMaxWidth()
subtitle = null, .background(MaterialTheme.colorScheme.background),
onClick = {}, ) {
onLongClick = null, HorizontalDivider()
modifier = Modifier RelatedMangaTitle(
.padding( title = stringResource(MR.strings.loading),
start = MaterialTheme.padding.small, subtitle = null,
end = MaterialTheme.padding.medium, onClick = {},
onLongClick = null,
modifier = Modifier
.padding(
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.small,
),
) )
.background(MaterialTheme.colorScheme.background), }
) }
item(key = "${relatedManga.hashCode()}#loading") { RelatedMangasLoadingItem() }
} }
item(key = "${related.hashCode()}#content") { RelatedMangasLoadingItem() } is RelatedManga.Success -> {
} else { val hasKeyword = relatedManga.keyword.isNotBlank()
val relatedManga = related as RelatedManga.Success stickyHeader(key = "$STICKY_HEADER_KEY_PREFIX-${relatedManga.hashCode()}#header") {
item(key = "${related.hashCode()}#divider") { HorizontalDivider() } Column(
stickyHeader(key = "$STICKY_HEADER_KEY_PREFIX${related.hashCode()}#header") { modifier = Modifier
RelatedMangaTitle( .fillMaxWidth()
title = if (relatedManga.keyword.isNotBlank()) { .background(MaterialTheme.colorScheme.background),
stringResource(KMR.strings.related_mangas_more) ) {
} else { HorizontalDivider()
stringResource(KMR.strings.related_mangas_website_suggestions) RelatedMangaTitle(
}, title = if (hasKeyword) {
showArrow = relatedManga.keyword.isNotBlank(), stringResource(KMR.strings.related_mangas_more)
subtitle = null, } else {
onClick = { stringResource(KMR.strings.related_mangas_website_suggestions)
if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword) },
}, showArrow = hasKeyword,
onLongClick = { subtitle = null,
if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword) onClick = {
}, if (hasKeyword) onKeywordClick(relatedManga.keyword)
modifier = Modifier },
.padding( onLongClick = {
start = MaterialTheme.padding.small, if (hasKeyword) onKeywordLongClick(relatedManga.keyword)
end = MaterialTheme.padding.medium, },
modifier = Modifier
.padding(
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.small,
),
) )
.background(MaterialTheme.colorScheme.background), }
) }
} items(
items( key = { "related-list-${relatedManga.mangaList[it].id}" },
key = { "related-list-${relatedManga.mangaList[it].id}" }, count = relatedManga.mangaList.size,
count = relatedManga.mangaList.size, ) { index ->
) { index -> val manga by getManga(relatedManga.mangaList[index])
val manga by getManga(relatedManga.mangaList[index]) BrowseSourceListItem(
BrowseSourceListItem( manga = manga,
manga = manga, onClick = { onMangaClick(manga) },
onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) },
onLongClick = { onMangaLongClick(manga) }, isSelected = selection.fastAny { selected -> selected.id == manga.id },
isSelected = selection.fastAny { selected -> selected.id == manga.id }, metadata = null,
metadata = null, )
) }
} }
} }
} }

View file

@ -32,6 +32,8 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController
@ -54,6 +56,7 @@ fun AnimatedFloatingSearchBox(
searchQuery: String?, searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onGloballyPositioned: (LayoutCoordinates) -> Unit = { },
placeholderText: String? = null, placeholderText: String? = null,
focusManager: FocusManager = LocalFocusManager.current, focusManager: FocusManager = LocalFocusManager.current,
focusRequester: FocusRequester = remember { FocusRequester() }, focusRequester: FocusRequester = remember { FocusRequester() },
@ -68,6 +71,7 @@ fun AnimatedFloatingSearchBox(
SourcesSearchBox( SourcesSearchBox(
searchQuery = searchQuery, searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
onGloballyPositioned = onGloballyPositioned,
placeholderText = placeholderText, placeholderText = placeholderText,
focusManager = focusManager, focusManager = focusManager,
focusRequester = focusRequester, focusRequester = focusRequester,
@ -81,6 +85,7 @@ fun SourcesSearchBox(
searchQuery: String?, searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onGloballyPositioned: (LayoutCoordinates) -> Unit = { },
placeholderText: String? = null, placeholderText: String? = null,
focusManager: FocusManager = LocalFocusManager.current, focusManager: FocusManager = LocalFocusManager.current,
focusRequester: FocusRequester = remember { FocusRequester() }, focusRequester: FocusRequester = remember { FocusRequester() },
@ -108,6 +113,7 @@ fun SourcesSearchBox(
value = searchQuery ?: "", value = searchQuery ?: "",
onValueChange = onChangeSearchQuery, onValueChange = onChangeSearchQuery,
modifier = modifier modifier = modifier
.onGloballyPositioned(onGloballyPositioned)
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onFocusChanged { isFocused = it.isFocused } .onFocusChanged { isFocused = it.isFocused }
@ -207,6 +213,8 @@ fun SearchBoxTrailingIcon(
} }
} }
internal val SOURCE_SEARCH_BOX_HEIGHT = 65.dp
@Preview @Preview
@Composable @Composable
fun PreviewClearFocusOnKeyboardDismissExample() { fun PreviewClearFocusOnKeyboardDismissExample() {

View file

@ -57,7 +57,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@ -74,6 +73,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.SOURCE_SEARCH_BOX_HEIGHT
import eu.kanade.presentation.components.SourcesSearchBox import eu.kanade.presentation.components.SourcesSearchBox
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
@ -194,10 +194,10 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
}, },
) { contentPadding -> ) { contentPadding ->
// KMK --> // KMK -->
val density = LocalDensity.current
Box(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) { Box(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) {
var searchBoxHeight by remember { mutableIntStateOf(40) } var searchBoxHeight by remember { mutableIntStateOf(with(density) { SOURCE_SEARCH_BOX_HEIGHT.roundToPx() }) }
// KMK <-- // KMK <--
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() } val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
// KMK --> // KMK -->
@ -234,12 +234,14 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
} }
// KMK --> // KMK -->
SourcesSearchBox( SourcesSearchBox(
modifier = Modifier
.onSizeChanged { searchBoxHeight = it.height }
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = MaterialTheme.padding.small),
searchQuery = searchQuery, searchQuery = searchQuery,
onChangeSearchQuery = { searchQuery = it ?: "" }, onChangeSearchQuery = { searchQuery = it ?: "" },
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = MaterialTheme.padding.small),
onGloballyPositioned = { layoutCoordinates ->
searchBoxHeight = layoutCoordinates.size.height
},
placeholderText = stringResource(KMR.strings.action_search_for_source), placeholderText = stringResource(KMR.strings.action_search_for_source),
) )
// KMK <-- // KMK <--

View file

@ -30,6 +30,7 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.CollapsibleBox import tachiyomi.presentation.core.components.CollapsibleBox
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
import tachiyomi.presentation.core.components.SelectItem import tachiyomi.presentation.core.components.SelectItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TextItem import tachiyomi.presentation.core.components.TextItem
@ -62,7 +63,9 @@ fun SourceFilterDialog(
AdaptiveSheet(onDismissRequest = onDismissRequest) { AdaptiveSheet(onDismissRequest = onDismissRequest) {
LazyColumn { LazyColumn {
stickyHeader { stickyHeader(
key = "$STICKY_HEADER_KEY_PREFIX-title",
) {
Row( Row(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)

View file

@ -28,7 +28,7 @@ import uy.kohesive.injekt.api.get
* A screen that displays a colors palette of current theme. * A screen that displays a colors palette of current theme.
*/ */
class PaletteScreen( class PaletteScreen(
@ColorInt private val seedColor: Int?, @param:ColorInt private val seedColor: Int?,
) : Screen() { ) : Screen() {
@Composable @Composable
@ -59,8 +59,7 @@ class PaletteScreen(
}, },
) { contentPadding -> ) { contentPadding ->
ScrollbarLazyColumn( ScrollbarLazyColumn(
// Using modifier instead of contentPadding so we can use stickyHeader contentPadding = contentPadding,
modifier = Modifier.padding(contentPadding),
) { ) {
item { item {
ButtonsColor( ButtonsColor(