Option to not show Related titles by default but using a button to open that screen

This commit is contained in:
Cuong M. Tran 2024-06-12 13:11:07 +07:00
parent 9a39a66dbc
commit 95fef0d019
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
11 changed files with 220 additions and 77 deletions

View file

@ -45,6 +45,8 @@ class UiPreferences(
fun feedTabInFront() = preferenceStore.getBoolean("latest_tab_position", false)
fun expandRelatedTitles() = preferenceStore.getBoolean("expand_related_titles", true)
fun recommendsInOverflow() = preferenceStore.getBoolean("recommends_in_overflow", true)
fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true)

View file

@ -17,19 +17,19 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.components.RelatedMangasComfortableGrid
import eu.kanade.presentation.browse.components.RelatedMangasCompactGrid
import eu.kanade.presentation.browse.components.RelatedMangasList
import eu.kanade.tachiyomi.ui.manga.RelatedManga
import exh.util.capitalize
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
@ -115,12 +115,14 @@ fun RelatedMangaTitle(
onClick: () -> Unit,
onLongClick: (() -> Unit)?,
modifier: Modifier = Modifier,
showArrow: Boolean = true,
) {
Row(
modifier = modifier
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.extraSmall,
end = MaterialTheme.padding.small,
top = MaterialTheme.padding.small,
)
.fillMaxWidth()
.let {
@ -130,7 +132,7 @@ fun RelatedMangaTitle(
it.combinedClickable(onClick = onClick, onLongClick = onLongClick)
}
},
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = if (showArrow) Arrangement.SpaceBetween else Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
@ -140,14 +142,14 @@ fun RelatedMangaTitle(
),
) {
Text(
text = title.capitalize(),
text = title.capitalize(Locale.current),
style = MaterialTheme.typography.titleMedium,
)
if (subtitle != null) {
Text(text = subtitle)
}
}
IconButton(onClick = onClick) {
if (showArrow) {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
}
}

View file

@ -5,6 +5,7 @@ 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.LazyVerticalGrid
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@ -56,6 +57,7 @@ fun RelatedMangasComfortableGrid(
header(key = "$related#content") { RelatedMangasLoadingItem() }
} else {
val relatedManga = related as RelatedManga.Success
header(key = "${related.keyword}#divider") { HorizontalDivider() }
header(key = "${related.keyword}#header") {
RelatedMangaTitle(
title = if (relatedManga.keyword.isNotBlank()) {

View file

@ -5,6 +5,7 @@ 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.LazyVerticalGrid
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@ -56,6 +57,7 @@ fun RelatedMangasCompactGrid(
header(key = "$related#content") { RelatedMangasLoadingItem() }
} else {
val relatedManga = related as RelatedManga.Success
header(key = "${related.keyword}#divider") { HorizontalDivider() }
header(key = "${related.keyword}#header") {
RelatedMangaTitle(
title = if (relatedManga.keyword.isNotBlank()) {

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@ -39,6 +40,7 @@ fun RelatedMangasList(
relatedMangas.forEach { related ->
val isLoading = related is RelatedManga.Loading
if (isLoading) {
item(key = "$related#divider") { HorizontalDivider() }
stickyHeader(key = "$related#header") {
RelatedMangaTitle(
title = stringResource(MR.strings.loading),
@ -51,6 +53,7 @@ fun RelatedMangasList(
item(key = "$related#content") { RelatedMangasLoadingItem() }
} else {
val relatedManga = related as RelatedManga.Success
item(key = "${related.keyword}#divider") { HorizontalDivider() }
stickyHeader(key = "${related.keyword}#header") {
RelatedMangaTitle(
title = if (relatedManga.keyword.isNotBlank()) {

View file

@ -26,6 +26,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
@ -51,6 +52,7 @@ import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.browse.RelatedMangaTitle
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.manga.components.ChapterDownloadAction
@ -63,6 +65,7 @@ import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaInfoButtons
import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
import eu.kanade.presentation.manga.components.OutlinedButtonWithArrow
import eu.kanade.presentation.manga.components.PagePreviewItems
import eu.kanade.presentation.manga.components.PagePreviews
import eu.kanade.presentation.manga.components.RelatedMangas
@ -382,6 +385,7 @@ private fun MangaScreenSmallImpl(
// SY <--
// KMK -->
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
val expandRelatedMangas = Injekt.get<UiPreferences>().expandRelatedTitles().get()
// KMK <--
val internalOnBackPressed = {
@ -578,26 +582,40 @@ private fun MangaScreenSmallImpl(
// KMK -->
if (state.source !is StubSource && relatedMangasEnabled) {
item(
key = MangaScreenItem.RELATED_TITLES,
contentType = MangaScreenItem.RELATED_TITLES,
) {
Column {
RelatedMangaTitle(
title = stringResource(KMR.strings.pref_source_related_mangas),
subtitle = null,
onClick = onRelatedMangasScreenClick,
onLongClick = null,
)
if (state.relatedMangasSorted == null || state.relatedMangasSorted.isNotEmpty()) {
RelatedMangas(
relatedMangas = state.relatedMangasSorted,
getMangaState = getMangaState,
onMangaClick = onRelatedMangaClick,
onMangaLongClick = onRelatedMangaLongClick,
if (expandRelatedMangas) {
item { HorizontalDivider() }
item(
key = MangaScreenItem.RELATED_TITLES,
contentType = MangaScreenItem.RELATED_TITLES,
) {
Column {
RelatedMangaTitle(
title = stringResource(KMR.strings.pref_source_related_mangas),
subtitle = null,
onClick = onRelatedMangasScreenClick,
onLongClick = null,
)
if (state.relatedMangasSorted?.isNotEmpty() != false) {
RelatedMangas(
relatedMangas = state.relatedMangasSorted,
getMangaState = getMangaState,
onMangaClick = onRelatedMangaClick,
onMangaLongClick = onRelatedMangaLongClick,
)
}
}
}
item { HorizontalDivider() }
} else {
item(
key = MangaScreenItem.RELATED_TITLES,
contentType = MangaScreenItem.RELATED_TITLES,
) {
OutlinedButtonWithArrow(
text = stringResource(KMR.strings.pref_source_related_mangas),
onClick = onRelatedMangasScreenClick,
)
}
}
}
// KMK <--
@ -747,6 +765,7 @@ private fun MangaScreenLargeImpl(
// SY <--
// KMK -->
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
val expandRelatedMangas = Injekt.get<UiPreferences>().expandRelatedTitles().get()
// KMK <--
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
@ -947,22 +966,38 @@ private fun MangaScreenLargeImpl(
) {
// KMK -->
if (state.source !is StubSource && relatedMangasEnabled) {
item(
key = MangaScreenItem.RELATED_TITLES,
contentType = MangaScreenItem.RELATED_TITLES,
) {
Column {
RelatedMangaTitle(
title = stringResource(KMR.strings.pref_source_related_mangas),
subtitle = null,
if (expandRelatedMangas) {
item { HorizontalDivider() }
item(
key = MangaScreenItem.RELATED_TITLES,
contentType = MangaScreenItem.RELATED_TITLES,
) {
Column {
RelatedMangaTitle(
title = stringResource(KMR.strings.pref_source_related_mangas),
subtitle = null,
onClick = onRelatedMangasScreenClick,
onLongClick = null,
)
if (state.relatedMangasSorted?.isNotEmpty() != false) {
RelatedMangas(
relatedMangas = state.relatedMangasSorted,
getMangaState = getMangaState,
onMangaClick = onRelatedMangaClick,
onMangaLongClick = onRelatedMangaLongClick,
)
}
}
}
item { HorizontalDivider() }
} else {
item(
key = MangaScreenItem.RELATED_TITLES,
contentType = MangaScreenItem.RELATED_TITLES,
) {
OutlinedButtonWithArrow(
text = stringResource(KMR.strings.pref_source_related_mangas),
onClick = onRelatedMangasScreenClick,
onLongClick = null,
)
RelatedMangas(
relatedMangas = state.relatedMangasSorted,
getMangaState = getMangaState,
onMangaClick = onRelatedMangaClick,
onMangaLongClick = onRelatedMangaLongClick
)
}
}

View file

@ -0,0 +1,64 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import tachiyomi.presentation.core.components.material.padding
@Composable
fun OutlinedButtonWithArrow(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector = Icons.AutoMirrored.Filled.ArrowForward,
) {
OutlinedButton(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.padding(MaterialTheme.padding.small),
colors = ButtonDefaults.outlinedButtonColors(),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = text.uppercase(),
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = icon,
contentDescription = "Arrow"
)
}
}
}
@Preview(showBackground = true)
@Composable
fun OutlinedButtonWithArrowPreview() {
OutlinedButtonWithArrow(
text = "Click Me",
onClick = { /* Do nothing */ }
)
}

View file

@ -13,6 +13,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.materialkolor.PaletteStyle
import eu.kanade.core.preference.asState
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
@ -196,6 +197,7 @@ object SettingsAppearanceScreen : SearchableSettings {
@Composable
fun getForkGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
val previewsRowCount by uiPreferences.previewsRowCount().collectAsState()
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
return Preference.PreferenceGroup(
stringResource(SYMR.strings.pref_category_fork),
@ -204,6 +206,12 @@ object SettingsAppearanceScreen : SearchableSettings {
pref = uiPreferences.expandFilters(),
title = stringResource(SYMR.strings.toggle_expand_search_filters),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.expandRelatedTitles(),
title = stringResource(KMR.strings.pref_expand_related_titles),
subtitle = stringResource(KMR.strings.pref_expand_related_titles_summary),
enabled = sourcePreferences.relatedMangas().get(),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.recommendsInOverflow(),
title = stringResource(SYMR.strings.put_recommends_in_overflow),

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
@ -90,6 +91,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withNonCancellableContext
@ -282,6 +284,9 @@ class MangaScreen(
// KMK -->
getMangaState = { screenModel.getManga(initialManga = it) },
onRelatedMangasScreenClick = {
if (successState.isRelatedMangasFetched == null) {
screenModel.screenModelScope.launchIO { screenModel.fetchRelatedMangasFromSource(onDemand = true) }
}
navigator.push(
RelatedMangasScreen(
mangaScreenModel = screenModel,

View file

@ -172,8 +172,9 @@ class MangaScreenModel(
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
readerPreferences: ReaderPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(),
private val uiPreferences: UiPreferences = Injekt.get(),
// KMK -->
private val sourcePreferences: SourcePreferences = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(),
private val trackChapter: TrackChapter = Injekt.get(),
// KMK <--
@ -461,18 +462,14 @@ class MangaScreenModel(
// Fetch info-chapters when needed
if (screenModelScope.isActive) {
// KMK -->
launch {
fetchRelatedMangasFromSource(onFinish = {
updateSuccessState { it.copy(isFetchingRelatedMangas = false) }
})
}
// KMK <--
val fetchFromSourceTasks = listOf(
async { syncTrackers() },
async { if (needRefreshInfo) fetchMangaFromSource() },
async { if (needRefreshChapter) fetchChaptersFromSource() },
)
// KMK -->
launch { fetchRelatedMangasFromSource() }
// KMK <--
fetchFromSourceTasks.awaitAll()
}
@ -1115,7 +1112,7 @@ class MangaScreenModel(
val isLocal = manga.isLocal()
// SY -->
val isExhManga = manga.isEhBasedManga()
val enabledLanguages = Injekt.get<SourcePreferences>().enabledLanguages().get()
val enabledLanguages = sourcePreferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
// SY <--
return map { chapter ->
@ -1219,10 +1216,26 @@ class MangaScreenModel(
}
// KMK -->
/**
* Set the fetching related mangas status.
* @param state
* - false: started & fetching
* - true: finished
*/
private fun setRelatedMangasFetchedStatus(state: Boolean) {
updateSuccessState { it.copy(isRelatedMangasFetched = state) }
}
/**
* Requests an list of related mangas from the source.
*/
private suspend fun fetchRelatedMangasFromSource(onFinish: () -> Unit) {
internal suspend fun fetchRelatedMangasFromSource(onDemand: Boolean = false, onFinish: (() -> Unit)? = null) {
val expandRelatedTitles = uiPreferences.expandRelatedTitles().get()
if (!onDemand && !expandRelatedTitles) return
// start fetching related mangas
setRelatedMangasFetchedStatus(false)
fun exceptionHandler(e: Throwable) {
logcat(LogPriority.ERROR, e)
val message = with(context) { e.formattedMessage }
@ -1232,33 +1245,36 @@ class MangaScreenModel(
}
}
val state = successState ?: return
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
try {
if (state.source !is StubSource) {
if (relatedMangasEnabled) {
state.source.getRelatedMangaList(state.manga.toSManga(), { e -> exceptionHandler(e) }) { pair, _ ->
/* Push found related mangas into collection */
val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList ->
mangaList.map {
networkToLocalManga.await(it.toDomainManga(state.source.id))
}
}
val relatedMangasEnabled = sourcePreferences.relatedMangas().get()
updateSuccessState { successState ->
val relatedMangaCollection =
successState.relatedMangaCollection
?.toMutableStateList()
?.apply { add(relatedManga) }
?: listOf(relatedManga)
successState.copy(relatedMangaCollection = relatedMangaCollection)
try {
if (state.source !is StubSource && relatedMangasEnabled) {
state.source.getRelatedMangaList(state.manga.toSManga(), { e -> exceptionHandler(e) }) { pair, _ ->
/* Push found related mangas into collection */
val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList ->
mangaList.map {
networkToLocalManga.await(it.toDomainManga(state.source.id))
}
}
updateSuccessState { successState ->
val relatedMangaCollection =
successState.relatedMangaCollection
?.toMutableStateList()
?.apply { add(relatedManga) }
?: listOf(relatedManga)
successState.copy(relatedMangaCollection = relatedMangaCollection)
}
}
}
} catch (e: Exception) {
exceptionHandler(e)
} finally {
onFinish()
if (onFinish != null) {
onFinish()
} else {
setRelatedMangasFetchedStatus(true)
}
}
}
// KMK <--
@ -1870,7 +1886,13 @@ class MangaScreenModel(
val previewsRowCount: Int,
// SY <--
// KMK -->
val isFetchingRelatedMangas: Boolean = true,
/**
* status of fetching related mangas
* - null: not started
* - false: started & fetching
* - true: finished
*/
val isRelatedMangasFetched: Boolean? = null,
/**
* a list of <keyword, related mangas>
*/
@ -1888,8 +1910,8 @@ class MangaScreenModel(
?.sorted(manga)
?.removeDuplicates(manga)
?.filter { it.isVisible() }
?.isLoading(isFetchingRelatedMangas)
?: if (!isFetchingRelatedMangas) emptyList() else null
?.isLoading(isRelatedMangasFetched)
?: if (isRelatedMangasFetched == true) emptyList() else null
// KMK <--
val processedChapters by lazy {
@ -2051,12 +2073,8 @@ sealed interface RelatedManga {
}
}
internal fun List<RelatedManga>.isLoading(isFetchingRelatedMangas: Boolean): List<RelatedManga> {
return if (isFetchingRelatedMangas) {
this + listOf(Loading)
} else {
this
}
internal fun List<RelatedManga>.isLoading(isRelatedMangaFetched: Boolean?): List<RelatedManga> {
return if (isRelatedMangaFetched == false) this + listOf(Loading) else this
}
}
}

View file

@ -12,6 +12,8 @@
<string name="pref_details_page_theme_cover_based">Details page theme based on cover</string>
<string name="pref_theme_cover_based_style">Theme style</string>
<string name="pref_theme_cover_based_animate">Theme animate</string>
<string name="pref_expand_related_titles">Expand Related titles</string>
<string name="pref_expand_related_titles_summary">If turned off, related titles won\'t automatically load &amp; show expanded</string>
<!-- Extension section -->
<string name="ext_unofficial">Unofficial</string>
@ -58,7 +60,7 @@
<string name="changelogs">Change Logs</string>
<!-- Browse settings -->
<string name="pref_source_related_mangas">Related titles</string>
<string name="pref_source_related_mangas">Related Titles</string>
<string name="pref_source_related_mangas_summary">Show related titles while viewing entry</string>
<string name="related_mangas_more">More like these...</string>