Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ object MangaConstants {
val allUploaders: ImmutableSet<String> = persistentSetOf(),
val allSources: ImmutableSet<String> = persistentSetOf(),
val allLanguages: ImmutableSet<String> = persistentSetOf(),
val chapterVolumes: Map<Long, String> = emptyMap(),
)

data class MangaScreenTrackState(
Expand Down
79 changes: 62 additions & 17 deletions app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,24 @@ class MangaViewModel(val mangaId: Long) : ViewModel() {
.distinctUntilChanged()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 1)

private data class AggregateResult(
val aggregate: eu.kanade.tachiyomi.data.database.models.MangaAggregate?
)

val aggregateFlow =
db.getMangaAggregate(mangaId)
.asRxObservable()
.map { AggregateResult(it) }
.asFlow()
.map { result ->
val dbAggregate = result.aggregate
if (dbAggregate != null) {
Json.parseToJsonElement(dbAggregate.volumes).asMdMap<AggregateVolume>()
} else null
}
.distinctUntilChanged()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 1)

val historyFlow =
db.getHistoryByMangaId(mangaId)
.asFlow()
Expand Down Expand Up @@ -427,10 +445,12 @@ class MangaViewModel(val mangaId: Long) : ViewModel() {
historyFlow,
::Triple,
),
aggregateFlow,
) {
(mangaItem, staticChapterData, categoriesData),
(artworkList, tracks, IsMerged),
(filterState, dynamicCover, history) ->
(filterState, dynamicCover, history),
aggregateVolumes ->
withContext(Dispatchers.Default) {
val effectiveManga =
if (filterState != null) {
Expand Down Expand Up @@ -495,6 +515,12 @@ class MangaViewModel(val mangaId: Long) : ViewModel() {

val nextUnread = getNextUnread(effectiveManga, activeChapters)

val chapterVolumes =
staticChapterData.allChapters.associate { chapterItem ->
chapterItem.chapter.id to
getVolumeForChapter(chapterItem.chapter, aggregateVolumes)
}

val allChapterInfo =
AllChapterInfo(
nextUnread = nextUnread,
Expand All @@ -505,6 +531,7 @@ class MangaViewModel(val mangaId: Long) : ViewModel() {
allUploaders = staticChapterData.allUploaders,
allSources = staticChapterData.allSources,
allLanguages = staticChapterData.allLanguages,
chapterVolumes = chapterVolumes,
)

val loggedInTrackerService =
Expand Down Expand Up @@ -617,6 +644,7 @@ class MangaViewModel(val mangaId: Long) : ViewModel() {
),
chapters =
it.chapters.copy(
chapterVolumes = allInfo.allChapterInfo.chapterVolumes,
activeChapters = allInfo.allChapterInfo.activeChapters,
nextUnreadChapter = allInfo.allChapterInfo.nextUnread,
chapterFilter = allInfo.chapterDisplay,
Expand Down Expand Up @@ -2401,22 +2429,9 @@ class MangaViewModel(val mangaId: Long) : ViewModel() {
null
}

if (volumes != null) {
for ((_, volumeInfo) in volumes) {
val chaptersInVolume = volumeInfo.chapters.values
val matchById =
mangaDexChapterId != null &&
chaptersInVolume.any {
it.id == mangaDexChapterId ||
it.others.contains(mangaDexChapterId)
}
val matchByNumber = chaptersInVolume.any { it.chapter == chapterNumberStr }

if (matchById || matchByNumber) {
volumeFromAggregate = volumeInfo.volume
break
}
}
val vol = getVolumeForChapter(chapter, volumes)
if (vol != "No Volume") {
volumeFromAggregate = vol
}
}
}
Expand Down Expand Up @@ -2495,4 +2510,34 @@ private data class AllChapterInfo(
val allUploaders: PersistentSet<String> = persistentSetOf(),
val allSources: PersistentSet<String> = persistentSetOf(),
val allLanguages: PersistentSet<String> = persistentSetOf(),
val chapterVolumes: Map<Long, String> = emptyMap(),
)

private fun getVolumeForChapter(
chapter: SimpleChapter,
volumes: Map<String, AggregateVolume>?,
): String {
if (volumes == null) return "No Volume"
val mangaDexChapterId = chapter.mangaDexChapterId
val chapterNumber = chapter.chapterNumber
val chapterNumberStr =
if (chapterNumber % 1 == 0f) {
chapterNumber.toInt().toString()
} else {
chapterNumber.toString()
}
for ((_, volumeInfo) in volumes) {
val chaptersInVolume = volumeInfo.chapters.values
val matchById =
mangaDexChapterId != null &&
chaptersInVolume.any {
it.id == mangaDexChapterId || it.others.contains(mangaDexChapterId)
}
val matchByNumber = chaptersInVolume.any { it.chapter == chapterNumberStr }

if (matchById || matchByNumber) {
return volumeInfo.volume
}
}
return "none"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.nekomanga.presentation.components.listcard

import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
Expand All @@ -23,6 +25,8 @@ enum class ListCardType {
fun ExpressiveListCard(
modifier: Modifier = Modifier,
listCardType: ListCardType,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
themeColorState: ThemeColorState = defaultThemeColorState(),
content: @Composable () -> Unit,
) {
Expand Down Expand Up @@ -72,5 +76,16 @@ fun ExpressiveListCard(
}
}

ElevatedCard(modifier = modifier, shape = shape, colors = colors) { content() }
ElevatedCard(modifier = modifier, shape = shape, colors = colors) {
Box(
modifier =
Modifier.combinedClickable(
enabled = onClick != null || onLongClick != null,
onClick = { onClick?.invoke() },
onLongClick = { onLongClick?.invoke() },
)
) {
content()
}
}
}
125 changes: 103 additions & 22 deletions app/src/main/java/org/nekomanga/presentation/screens/MangaScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,29 @@ import android.content.Intent
import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState
Expand All @@ -28,10 +38,12 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
Expand Down Expand Up @@ -69,7 +81,6 @@ import eu.kanade.tachiyomi.util.system.sharedCacheDir
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.withUIContext
import java.text.DateFormat
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.nekomanga.R
Expand All @@ -83,6 +94,8 @@ import org.nekomanga.presentation.components.VerticalDivider
import org.nekomanga.presentation.components.VerticalFastScroller
import org.nekomanga.presentation.components.dialog.RemovedChaptersDialog
import org.nekomanga.presentation.components.dynamicTextSelectionColor
import org.nekomanga.presentation.components.listcard.ExpressiveListCard
import org.nekomanga.presentation.components.listcard.ListCardType
import org.nekomanga.presentation.components.nekoRippleConfiguration
import org.nekomanga.presentation.components.scaffold.ChildScreenScaffold
import org.nekomanga.presentation.components.snackbar.NekoSnackbarHost
Expand Down Expand Up @@ -590,7 +603,8 @@ private fun MangaScreenWrapper(
}

private fun LazyListScope.chapterList(
chapters: PersistentList<ChapterItem>,
collapsedVolumes: SnapshotStateMap<String, Boolean>,
chapterGroups: Map<String, List<ChapterItem>>,
screenState: MangaConstants.MangaDetailScreenState,
themeColorState: ThemeColorState,
chapterActions: ChapterActions,
Expand All @@ -599,27 +613,80 @@ private fun LazyListScope.chapterList(
onOpenSheet: (DetailsBottomSheetScreen) -> Unit,
) {
item(key = "chapter_header") {
val totalChapters = chapterGroups.values.sumOf { it.size }
ChapterHeader(
themeColor = themeColorState,
numberOfChapters = chapters.size,
numberOfChapters = totalChapters,
filterText = screenState.chapters.chapterFilterText,
onClick = { onOpenSheet(DetailsBottomSheetScreen.FilterChapterSheet) },
)
}

itemsIndexed(items = chapters, key = { _, chapter -> chapter.chapter.id }) { index, chapterItem
->
MangaChapterListItem(
index = index,
chapterItem = chapterItem,
count = chapters.size,
themeColorState = themeColorState,
shouldHideChapterTitles =
screenState.chapters.chapterFilter.hideChapterTitles == ToggleableState.On,
chapterActions = chapterActions,
onBookmark = onBookmark,
onRead = onRead,
)
chapterGroups.forEach { (volume, chapters) ->
val isCollapsed = collapsedVolumes[volume] ?: false

// 1. The Volume Header Card
item(key = "volume_header_$volume") {
ExpressiveListCard(
modifier = Modifier.padding(horizontal = Size.small),
listCardType = if (isCollapsed) ListCardType.Single else ListCardType.Top,
themeColorState = themeColorState,
onClick = { collapsedVolumes[volume] = !isCollapsed },
onLongClick = {
val anyExpanded = chapterGroups.keys.any { collapsedVolumes[it] != true }
chapterGroups.keys.forEach { collapsedVolumes[it] = anyExpanded }
},
) {
Row(
modifier =
Modifier.fillMaxWidth()
.padding(
top = Size.small,
bottom = Size.small,
start = Size.small,
end = Size.tiny,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = if (volume.equals("none", true)) "No Volume" else "Vol. $volume",
style = MaterialTheme.typography.titleMedium,
color = themeColorState.primaryColor,
)
Icon(
imageVector =
if (isCollapsed) Icons.Default.ExpandMore else Icons.Default.ExpandLess,
contentDescription = null,
tint = themeColorState.primaryColor,
)
}
}
Spacer(modifier = Modifier.size(Size.tiny))
}

// 2. The Chapters (Only shown if NOT collapsed)
if (!isCollapsed) {
// We use itemsIndexed so each chapter is its own lazy list item with its own shape
itemsIndexed(
items = chapters,
key = { _, chapter -> "vol_${volume}_ch_${chapter.chapter.id}" },
) { index, chapterItem ->
MangaChapterListItem(
// We adjust the index/count so the shapes (Top, Center, Bottom)
// look correct relative to the volume header
index = index + 1,
count = chapters.size + 1,
chapterItem = chapterItem,
themeColorState = themeColorState,
shouldHideChapterTitles =
screenState.chapters.chapterFilter.hideChapterTitles == ToggleableState.On,
chapterActions = chapterActions,
onBookmark = onBookmark,
onRead = onRead,
)
}
}
}
}

Expand Down Expand Up @@ -652,6 +719,8 @@ private fun VerticalLayout(
topContentPadding = incomingContentPadding.calculateTopPadding(),
bottomContentPadding = incomingContentPadding.calculateBottomPadding(),
) {
val collapsedVolumes = remember { mutableStateMapOf<String, Boolean>() }

LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
Expand Down Expand Up @@ -685,10 +754,15 @@ private fun VerticalLayout(
)
}
if (isInitialized) {
val chapters =
if (screenState.general.isSearching) screenState.general.searchChapters
else screenState.chapters.activeChapters
val chapterGroups = chapters.groupBy {
screenState.chapters.chapterVolumes[it.chapter.id] ?: "No Volume"
}
chapterList(
chapters =
if (screenState.general.isSearching) screenState.general.searchChapters
else screenState.chapters.activeChapters,
collapsedVolumes = collapsedVolumes,
chapterGroups = chapterGroups,
screenState = screenState,
themeColorState = themeColorState,
chapterActions = chapterActions,
Expand Down Expand Up @@ -776,12 +850,19 @@ private fun SideBySideLayout(
topContentPadding = incomingContentPadding.calculateTopPadding(),
bottomContentPadding = incomingContentPadding.calculateBottomPadding(),
) {
val collapsedVolumes = remember { mutableStateMapOf<String, Boolean>() }

LazyColumn(state = listState, contentPadding = chapterContentPadding) {
if (isInitialized) {
val chapters =
if (screenState.general.isSearching) screenState.general.searchChapters
else screenState.chapters.activeChapters
val chapterGroups = chapters.groupBy {
screenState.chapters.chapterVolumes[it.chapter.id] ?: "No Volume"
}
chapterList(
chapters =
if (screenState.general.isSearching) screenState.general.searchChapters
else screenState.chapters.activeChapters,
collapsedVolumes = collapsedVolumes,
chapterGroups = chapterGroups,
screenState = screenState,
themeColorState = themeColorState,
chapterActions = chapterActions,
Expand Down
Loading