mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-17 10:59:15 +01:00
Use Voyager on Library tab (#8620)
This commit is contained in:
parent
fe579c4865
commit
e14909fff4
@ -49,6 +49,9 @@ object CommonMangaItemDefaults {
|
||||
}
|
||||
|
||||
private val ContinueReadingButtonSize = 32.dp
|
||||
private val ContinueReadingButtonGridPadding = 6.dp
|
||||
private val ContinueReadingButtonListSpacing = 8.dp
|
||||
|
||||
private const val GridSelectedCoverAlpha = 0.76f
|
||||
|
||||
/**
|
||||
@ -61,9 +64,8 @@ fun MangaCompactGridItem(
|
||||
title: String? = null,
|
||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||
coverAlpha: Float = 1f,
|
||||
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
showContinueReadingButton: Boolean = false,
|
||||
coverBadgeStart: @Composable (RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
@ -86,12 +88,17 @@ fun MangaCompactGridItem(
|
||||
badgesEnd = coverBadgeEnd,
|
||||
content = {
|
||||
if (title != null) {
|
||||
CoverTextOverlay(title = title, showContinueReadingButton)
|
||||
}
|
||||
},
|
||||
continueReadingButton = {
|
||||
if (showContinueReadingButton && onClickContinueReading != null) {
|
||||
ContinueReadingButton(onClickContinueReading)
|
||||
CoverTextOverlay(
|
||||
title = title,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
)
|
||||
} else if (onClickContinueReading != null) {
|
||||
ContinueReadingButton(
|
||||
modifier = Modifier
|
||||
.padding(ContinueReadingButtonGridPadding)
|
||||
.align(Alignment.BottomEnd),
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -104,7 +111,7 @@ fun MangaCompactGridItem(
|
||||
@Composable
|
||||
private fun BoxScope.CoverTextOverlay(
|
||||
title: String,
|
||||
showContinueReadingButton: Boolean = false,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -119,11 +126,14 @@ private fun BoxScope.CoverTextOverlay(
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
val endPadding = if (showContinueReadingButton) ContinueReadingButtonSize else 0.dp
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
GridItemTitle(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, top = 8.dp, end = endPadding + 8.dp, bottom = 8.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
.weight(1f)
|
||||
.padding(8.dp),
|
||||
title = title,
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
color = Color.White,
|
||||
@ -133,6 +143,16 @@ private fun BoxScope.CoverTextOverlay(
|
||||
),
|
||||
),
|
||||
)
|
||||
if (onClickContinueReading != null) {
|
||||
ContinueReadingButton(
|
||||
modifier = Modifier.padding(
|
||||
end = ContinueReadingButtonGridPadding,
|
||||
bottom = ContinueReadingButtonGridPadding,
|
||||
),
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,7 +166,6 @@ fun MangaComfortableGridItem(
|
||||
coverAlpha: Float = 1f,
|
||||
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
showContinueReadingButton: Boolean = false,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
@ -168,9 +187,14 @@ fun MangaComfortableGridItem(
|
||||
},
|
||||
badgesStart = coverBadgeStart,
|
||||
badgesEnd = coverBadgeEnd,
|
||||
continueReadingButton = {
|
||||
if (showContinueReadingButton && onClickContinueReading != null) {
|
||||
ContinueReadingButton(onClickContinueReading)
|
||||
content = {
|
||||
if (onClickContinueReading != null) {
|
||||
ContinueReadingButton(
|
||||
modifier = Modifier
|
||||
.padding(ContinueReadingButtonGridPadding)
|
||||
.align(Alignment.BottomEnd),
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -192,7 +216,6 @@ private fun MangaGridCover(
|
||||
cover: @Composable BoxScope.() -> Unit = {},
|
||||
badgesStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
badgesEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
continueReadingButton: (@Composable BoxScope.() -> Unit)? = null,
|
||||
content: @Composable (BoxScope.() -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
@ -219,7 +242,6 @@ private fun MangaGridCover(
|
||||
content = badgesEnd,
|
||||
)
|
||||
}
|
||||
continueReadingButton?.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,8 +332,7 @@ fun MangaListItem(
|
||||
title: String,
|
||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||
coverAlpha: Float = 1f,
|
||||
badge: @Composable RowScope.() -> Unit,
|
||||
showContinueReadingButton: Boolean = false,
|
||||
badge: @Composable (RowScope.() -> Unit),
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
@ -343,23 +364,21 @@ fun MangaListItem(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
BadgeGroup(content = badge)
|
||||
if (showContinueReadingButton && onClickContinueReading != null) {
|
||||
Box {
|
||||
ContinueReadingButton(onClickContinueReading)
|
||||
}
|
||||
if (onClickContinueReading != null) {
|
||||
ContinueReadingButton(
|
||||
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ContinueReadingButton(
|
||||
private fun ContinueReadingButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onClickContinueReading: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp),
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
FilledIconButton(
|
||||
onClick = onClickContinueReading,
|
||||
modifier = Modifier.size(ContinueReadingButtonSize),
|
||||
|
@ -1,130 +0,0 @@
|
||||
package eu.kanade.presentation.library
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.util.fastAll
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.library.model.display
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.EmptyScreenAction
|
||||
import eu.kanade.presentation.components.LibraryBottomActionMenu
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.library.components.LibraryContent
|
||||
import eu.kanade.presentation.library.components.LibraryToolbar
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||
|
||||
@Composable
|
||||
fun LibraryScreen(
|
||||
presenter: LibraryPresenter,
|
||||
onMangaClicked: (Long) -> Unit,
|
||||
onContinueReadingClicked: (LibraryManga) -> Unit,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
onChangeCategoryClicked: () -> Unit,
|
||||
onMarkAsReadClicked: () -> Unit,
|
||||
onMarkAsUnreadClicked: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
onDeleteClicked: () -> Unit,
|
||||
onClickUnselectAll: () -> Unit,
|
||||
onClickSelectAll: () -> Unit,
|
||||
onClickInvertSelection: () -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
onClickRefresh: (Category?) -> Boolean,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
val title by presenter.getToolbarTitle()
|
||||
val tabVisible = presenter.tabVisibility && presenter.categories.size > 1
|
||||
LibraryToolbar(
|
||||
state = presenter,
|
||||
title = title,
|
||||
incognitoMode = !tabVisible && presenter.isIncognitoMode,
|
||||
downloadedOnlyMode = !tabVisible && presenter.isDownloadOnly,
|
||||
onClickUnselectAll = onClickUnselectAll,
|
||||
onClickSelectAll = onClickSelectAll,
|
||||
onClickInvertSelection = onClickInvertSelection,
|
||||
onClickFilter = onClickFilter,
|
||||
onClickRefresh = { onClickRefresh(null) },
|
||||
onClickOpenRandomManga = onClickOpenRandomManga,
|
||||
scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
LibraryBottomActionMenu(
|
||||
visible = presenter.selectionMode,
|
||||
onChangeCategoryClicked = onChangeCategoryClicked,
|
||||
onMarkAsReadClicked = onMarkAsReadClicked,
|
||||
onMarkAsUnreadClicked = onMarkAsUnreadClicked,
|
||||
onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.fastAll { !it.manga.isLocal() } },
|
||||
onDeleteClicked = onDeleteClicked,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (presenter.isLoading) {
|
||||
LoadingScreen()
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
val contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(paddingValues)
|
||||
if (presenter.searchQuery.isNullOrEmpty() && presenter.isLibraryEmpty) {
|
||||
val handler = LocalUriHandler.current
|
||||
EmptyScreen(
|
||||
textResource = R.string.information_empty_library,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
actions = listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.getting_started_guide,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
|
||||
),
|
||||
),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LibraryContent(
|
||||
state = presenter,
|
||||
contentPadding = contentPadding,
|
||||
currentPage = { presenter.activeCategory },
|
||||
isLibraryEmpty = presenter.isLibraryEmpty,
|
||||
showPageTabs = presenter.tabVisibility,
|
||||
showMangaCount = presenter.mangaCountVisibility,
|
||||
onChangeCurrentPage = { presenter.activeCategory = it },
|
||||
onMangaClicked = onMangaClicked,
|
||||
onContinueReadingClicked = onContinueReadingClicked,
|
||||
onToggleSelection = { presenter.toggleSelection(it) },
|
||||
onToggleRangeSelection = {
|
||||
presenter.toggleRangeSelection(it)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onRefresh = onClickRefresh,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) },
|
||||
getDisplayModeForPage = { presenter.categories[it].display },
|
||||
getColumnsForOrientation = { presenter.getColumnsPreferenceForCurrentOrientation(it) },
|
||||
getLibraryForPage = { presenter.getMangaForCategory(page = it) },
|
||||
showDownloadBadges = presenter.showDownloadBadges,
|
||||
showUnreadBadges = presenter.showUnreadBadges,
|
||||
showLocalBadges = presenter.showLocalBadges,
|
||||
showLanguageBadges = presenter.showLanguageBadges,
|
||||
showContinueReadingButton = presenter.showContinueReadingButton,
|
||||
isIncognitoMode = presenter.isIncognitoMode,
|
||||
isDownloadOnly = presenter.isDownloadOnly,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package eu.kanade.presentation.library
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||
|
||||
@Stable
|
||||
interface LibraryState {
|
||||
val isLoading: Boolean
|
||||
val categories: List<Category>
|
||||
var searchQuery: String?
|
||||
val selection: List<LibraryManga>
|
||||
val selectionMode: Boolean
|
||||
var hasActiveFilters: Boolean
|
||||
var dialog: LibraryPresenter.Dialog?
|
||||
}
|
||||
|
||||
fun LibraryState(): LibraryState {
|
||||
return LibraryStateImpl()
|
||||
}
|
||||
|
||||
class LibraryStateImpl : LibraryState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var categories: List<Category> by mutableStateOf(emptyList())
|
||||
override var searchQuery: String? by mutableStateOf(null)
|
||||
override var selection: List<LibraryManga> by mutableStateOf(emptyList())
|
||||
override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() }
|
||||
override var hasActiveFilters: Boolean by mutableStateOf(false)
|
||||
override var dialog: LibraryPresenter.Dialog? by mutableStateOf(null)
|
||||
}
|
@ -5,16 +5,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
|
||||
@Composable
|
||||
fun DownloadsBadge(
|
||||
enabled: Boolean,
|
||||
item: LibraryItem,
|
||||
) {
|
||||
if (enabled && item.downloadCount > 0) {
|
||||
fun DownloadsBadge(count: Int) {
|
||||
if (count > 0) {
|
||||
Badge(
|
||||
text = "${item.downloadCount}",
|
||||
text = "$count",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
@ -22,30 +18,26 @@ fun DownloadsBadge(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnreadBadge(
|
||||
enabled: Boolean,
|
||||
item: LibraryItem,
|
||||
) {
|
||||
if (enabled && item.unreadCount > 0) {
|
||||
Badge(text = "${item.unreadCount}")
|
||||
fun UnreadBadge(count: Int) {
|
||||
if (count > 0) {
|
||||
Badge(text = "$count")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LanguageBadge(
|
||||
showLanguage: Boolean,
|
||||
showLocal: Boolean,
|
||||
item: LibraryItem,
|
||||
isLocal: Boolean,
|
||||
sourceLanguage: String,
|
||||
) {
|
||||
if (showLocal && item.isLocal) {
|
||||
if (isLocal) {
|
||||
Badge(
|
||||
text = stringResource(R.string.local_source_badge),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
} else if (showLanguage && item.sourceLanguage.isNotEmpty()) {
|
||||
} else if (sourceLanguage.isNotEmpty()) {
|
||||
Badge(
|
||||
text = item.sourceLanguage.uppercase(),
|
||||
text = sourceLanguage.uppercase(),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
|
@ -14,17 +14,12 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
@Composable
|
||||
fun LibraryComfortableGrid(
|
||||
items: List<LibraryItem>,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
showLanguageBadges: Boolean,
|
||||
showContinueReadingButton: Boolean,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
searchQuery: String?,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
) {
|
||||
@ -51,26 +46,22 @@ fun LibraryComfortableGrid(
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverBadgeStart = {
|
||||
DownloadsBadge(
|
||||
enabled = showDownloadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
UnreadBadge(
|
||||
enabled = showUnreadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
DownloadsBadge(count = libraryItem.downloadCount.toInt())
|
||||
UnreadBadge(count = libraryItem.unreadCount.toInt())
|
||||
},
|
||||
coverBadgeEnd = {
|
||||
LanguageBadge(
|
||||
showLanguage = showLanguageBadges,
|
||||
showLocal = showLocalBadges,
|
||||
item = libraryItem,
|
||||
isLocal = libraryItem.isLocal,
|
||||
sourceLanguage = libraryItem.sourceLanguage,
|
||||
)
|
||||
},
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
onLongClick = { onLongClick(libraryItem.libraryManga) },
|
||||
onClick = { onClick(libraryItem.libraryManga) },
|
||||
onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
|
||||
onClickContinueReading = if (onClickContinueReading != null) {
|
||||
{ onClickContinueReading(libraryItem.libraryManga) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,17 +15,12 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
fun LibraryCompactGrid(
|
||||
items: List<LibraryItem>,
|
||||
showTitle: Boolean,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
showLanguageBadges: Boolean,
|
||||
showContinueReadingButton: Boolean,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
searchQuery: String?,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
) {
|
||||
@ -52,26 +47,22 @@ fun LibraryCompactGrid(
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverBadgeStart = {
|
||||
DownloadsBadge(
|
||||
enabled = showDownloadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
UnreadBadge(
|
||||
enabled = showUnreadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
DownloadsBadge(count = libraryItem.downloadCount.toInt())
|
||||
UnreadBadge(count = libraryItem.unreadCount.toInt())
|
||||
},
|
||||
coverBadgeEnd = {
|
||||
LanguageBadge(
|
||||
showLanguage = showLanguageBadges,
|
||||
showLocal = showLocalBadges,
|
||||
item = libraryItem,
|
||||
isLocal = libraryItem.isLocal,
|
||||
sourceLanguage = libraryItem.sourceLanguage,
|
||||
)
|
||||
},
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
onLongClick = { onLongClick(libraryItem.libraryManga) },
|
||||
onClick = { onClick(libraryItem.libraryManga) },
|
||||
onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
|
||||
onClickContinueReading = if (onClickContinueReading != null) {
|
||||
{ onClickContinueReading(libraryItem.libraryManga) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -21,7 +20,6 @@ import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.presentation.components.SwipeRefresh
|
||||
import eu.kanade.presentation.components.rememberPagerState
|
||||
import eu.kanade.presentation.library.LibraryState
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@ -29,28 +27,24 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun LibraryContent(
|
||||
state: LibraryState,
|
||||
categories: List<Category>,
|
||||
searchQuery: String?,
|
||||
selection: List<LibraryManga>,
|
||||
contentPadding: PaddingValues,
|
||||
currentPage: () -> Int,
|
||||
isLibraryEmpty: Boolean,
|
||||
showPageTabs: Boolean,
|
||||
showMangaCount: Boolean,
|
||||
onChangeCurrentPage: (Int) -> Unit,
|
||||
onMangaClicked: (Long) -> Unit,
|
||||
onContinueReadingClicked: (LibraryManga) -> Unit,
|
||||
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
||||
onToggleSelection: (LibraryManga) -> Unit,
|
||||
onToggleRangeSelection: (LibraryManga) -> Unit,
|
||||
onRefresh: (Category?) -> Boolean,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode,
|
||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||
getLibraryForPage: @Composable (Int) -> List<LibraryItem>,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
showLanguageBadges: Boolean,
|
||||
showContinueReadingButton: Boolean,
|
||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
||||
isDownloadOnly: Boolean,
|
||||
isIncognitoMode: Boolean,
|
||||
) {
|
||||
@ -61,38 +55,30 @@ fun LibraryContent(
|
||||
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
),
|
||||
) {
|
||||
val categories = state.categories
|
||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
||||
val pagerState = rememberPagerState(coercedCurrentPage)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||
|
||||
if (isLibraryEmpty.not() && showPageTabs && categories.size > 1) {
|
||||
if (!isLibraryEmpty && showPageTabs && categories.size > 1) {
|
||||
LibraryTabs(
|
||||
categories = categories,
|
||||
currentPageIndex = pagerState.currentPage,
|
||||
showMangaCount = showMangaCount,
|
||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
||||
isDownloadOnly = isDownloadOnly,
|
||||
isIncognitoMode = isIncognitoMode,
|
||||
onTabItemClick = { scope.launch { pagerState.animateScrollToPage(it) } },
|
||||
)
|
||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
||||
}
|
||||
|
||||
val notSelectionMode = selection.isEmpty()
|
||||
val onClickManga = { manga: LibraryManga ->
|
||||
if (state.selectionMode.not()) {
|
||||
if (notSelectionMode) {
|
||||
onMangaClicked(manga.manga.id)
|
||||
} else {
|
||||
onToggleSelection(manga)
|
||||
}
|
||||
}
|
||||
val onLongClickManga = { manga: LibraryManga ->
|
||||
onToggleRangeSelection(manga)
|
||||
}
|
||||
val onClickContinueReading = { manga: LibraryManga ->
|
||||
onContinueReadingClicked(manga)
|
||||
}
|
||||
|
||||
SwipeRefresh(
|
||||
refreshing = isRefreshing,
|
||||
@ -106,26 +92,21 @@ fun LibraryContent(
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = state.selectionMode.not(),
|
||||
enabled = notSelectionMode,
|
||||
) {
|
||||
LibraryPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||
pageCount = categories.size,
|
||||
selectedManga = state.selection,
|
||||
selectedManga = selection,
|
||||
searchQuery = searchQuery,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
getDisplayModeForPage = getDisplayModeForPage,
|
||||
getColumnsForOrientation = getColumnsForOrientation,
|
||||
getLibraryForPage = getLibraryForPage,
|
||||
showDownloadBadges = showDownloadBadges,
|
||||
showUnreadBadges = showUnreadBadges,
|
||||
showLocalBadges = showLocalBadges,
|
||||
showLanguageBadges = showLanguageBadges,
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
onClickManga = onClickManga,
|
||||
onLongClickManga = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
searchQuery = state.searchQuery,
|
||||
onLongClickManga = onToggleRangeSelection,
|
||||
onClickContinueReading = onContinueReadingClicked,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -23,16 +23,11 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
@Composable
|
||||
fun LibraryList(
|
||||
items: List<LibraryItem>,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
showLanguageBadges: Boolean,
|
||||
showContinueReadingButton: Boolean,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
searchQuery: String?,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
) {
|
||||
@ -41,13 +36,13 @@ fun LibraryList(
|
||||
contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
item {
|
||||
if (searchQuery.isNullOrEmpty().not()) {
|
||||
if (!searchQuery.isNullOrEmpty()) {
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onGlobalSearchClicked,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_global_search_query, searchQuery!!),
|
||||
text = stringResource(R.string.action_global_search_query, searchQuery),
|
||||
modifier = Modifier.zIndex(99f),
|
||||
)
|
||||
}
|
||||
@ -70,14 +65,20 @@ fun LibraryList(
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
badge = {
|
||||
DownloadsBadge(enabled = showDownloadBadges, item = libraryItem)
|
||||
UnreadBadge(enabled = showUnreadBadges, item = libraryItem)
|
||||
LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem)
|
||||
DownloadsBadge(count = libraryItem.downloadCount.toInt())
|
||||
UnreadBadge(count = libraryItem.unreadCount.toInt())
|
||||
LanguageBadge(
|
||||
isLocal = libraryItem.isLocal,
|
||||
sourceLanguage = libraryItem.sourceLanguage,
|
||||
)
|
||||
},
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
onLongClick = { onLongClick(libraryItem.libraryManga) },
|
||||
onClick = { onClick(libraryItem.libraryManga) },
|
||||
onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
|
||||
onClickContinueReading = if (onClickContinueReading != null) {
|
||||
{ onClickContinueReading(libraryItem.libraryManga) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -27,15 +27,10 @@ fun LibraryPager(
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode,
|
||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||
getLibraryForPage: @Composable (Int) -> List<LibraryItem>,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
showLanguageBadges: Boolean,
|
||||
showContinueReadingButton: Boolean,
|
||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
||||
onClickManga: (LibraryManga) -> Unit,
|
||||
onLongClickManga: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = pageCount,
|
||||
@ -62,11 +57,6 @@ fun LibraryPager(
|
||||
LibraryDisplayMode.List -> {
|
||||
LibraryList(
|
||||
items = library,
|
||||
showDownloadBadges = showDownloadBadges,
|
||||
showUnreadBadges = showUnreadBadges,
|
||||
showLocalBadges = showLocalBadges,
|
||||
showLanguageBadges = showLanguageBadges,
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
onClick = onClickManga,
|
||||
@ -80,11 +70,6 @@ fun LibraryPager(
|
||||
LibraryCompactGrid(
|
||||
items = library,
|
||||
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
||||
showDownloadBadges = showDownloadBadges,
|
||||
showUnreadBadges = showUnreadBadges,
|
||||
showLocalBadges = showLocalBadges,
|
||||
showLanguageBadges = showLanguageBadges,
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
@ -98,17 +83,12 @@ fun LibraryPager(
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
LibraryComfortableGrid(
|
||||
items = library,
|
||||
showDownloadBadges = showDownloadBadges,
|
||||
showUnreadBadges = showUnreadBadges,
|
||||
showLocalBadges = showLocalBadges,
|
||||
showLanguageBadges = showLanguageBadges,
|
||||
showContinueReadingButton = showContinueReadingButton,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
onClick = onClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
searchQuery = searchQuery,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
)
|
||||
|
@ -5,8 +5,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.presentation.category.visualName
|
||||
@ -19,10 +17,9 @@ import eu.kanade.presentation.components.TabText
|
||||
fun LibraryTabs(
|
||||
categories: List<Category>,
|
||||
currentPageIndex: Int,
|
||||
showMangaCount: Boolean,
|
||||
isDownloadOnly: Boolean,
|
||||
isIncognitoMode: Boolean,
|
||||
getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
@ -41,11 +38,7 @@ fun LibraryTabs(
|
||||
text = {
|
||||
TabText(
|
||||
text = category.visualName,
|
||||
badgeCount = if (showMangaCount) {
|
||||
getNumberOfMangaForCategory(category.id)
|
||||
} else {
|
||||
null
|
||||
}?.value,
|
||||
badgeCount = getNumberOfMangaForCategory(category),
|
||||
)
|
||||
},
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
|
@ -14,6 +14,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -23,13 +24,13 @@ import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.OverflowMenu
|
||||
import eu.kanade.presentation.components.Pill
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.presentation.library.LibraryState
|
||||
import eu.kanade.presentation.theme.active
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun LibraryToolbar(
|
||||
state: LibraryState,
|
||||
hasActiveFilters: Boolean,
|
||||
selectedCount: Int,
|
||||
title: LibraryToolbarTitle,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
@ -39,10 +40,12 @@ fun LibraryToolbar(
|
||||
onClickFilter: () -> Unit,
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
searchQuery: String?,
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
) = when {
|
||||
state.selectionMode -> LibrarySelectionToolbar(
|
||||
state = state,
|
||||
selectedCount > 0 -> LibrarySelectionToolbar(
|
||||
selectedCount = selectedCount,
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
onClickUnselectAll = onClickUnselectAll,
|
||||
@ -51,11 +54,11 @@ fun LibraryToolbar(
|
||||
)
|
||||
else -> LibraryRegularToolbar(
|
||||
title = title,
|
||||
hasFilters = state.hasActiveFilters,
|
||||
hasFilters = hasActiveFilters,
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
searchQuery = state.searchQuery,
|
||||
onChangeSearchQuery = { state.searchQuery = it },
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChange = onSearchQueryChange,
|
||||
onClickFilter = onClickFilter,
|
||||
onClickRefresh = onClickRefresh,
|
||||
onClickOpenRandomManga = onClickOpenRandomManga,
|
||||
@ -70,7 +73,7 @@ fun LibraryRegularToolbar(
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
searchQuery: String?,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
@ -96,7 +99,7 @@ fun LibraryRegularToolbar(
|
||||
}
|
||||
},
|
||||
searchQuery = searchQuery,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onChangeSearchQuery = onSearchQueryChange,
|
||||
actions = {
|
||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||
IconButton(onClick = onClickFilter) {
|
||||
@ -128,7 +131,7 @@ fun LibraryRegularToolbar(
|
||||
|
||||
@Composable
|
||||
fun LibrarySelectionToolbar(
|
||||
state: LibraryState,
|
||||
selectedCount: Int,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
onClickUnselectAll: () -> Unit,
|
||||
@ -136,7 +139,7 @@ fun LibrarySelectionToolbar(
|
||||
onClickInvertSelection: () -> Unit,
|
||||
) {
|
||||
AppBar(
|
||||
titleContent = { Text(text = "${state.selection.size}") },
|
||||
titleContent = { Text(text = "$selectedCount") },
|
||||
actions = {
|
||||
IconButton(onClick = onClickSelectAll) {
|
||||
Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
|
||||
@ -152,6 +155,7 @@ fun LibrarySelectionToolbar(
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class LibraryToolbarTitle(
|
||||
val text: String,
|
||||
val numberOfManga: Int? = null,
|
||||
|
@ -1,167 +1,37 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import eu.kanade.core.prefs.CheckboxState
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||
import eu.kanade.presentation.components.DeleteLibraryMangaDialog
|
||||
import eu.kanade.presentation.library.LibraryScreen
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LibraryController(
|
||||
bundle: Bundle? = null,
|
||||
) : FullComposeController<LibraryPresenter>(bundle), RootController {
|
||||
) : BasicFullComposeController(bundle), RootController {
|
||||
|
||||
/**
|
||||
* Sheet containing filter/sort/display items.
|
||||
*/
|
||||
private var settingsSheet: LibrarySettingsSheet? = null
|
||||
|
||||
override fun createPresenter(): LibraryPresenter = LibraryPresenter()
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val context = LocalContext.current
|
||||
val getMangaForCategory = presenter.getMangaForCategory(page = presenter.activeCategory)
|
||||
|
||||
LibraryScreen(
|
||||
presenter = presenter,
|
||||
onMangaClicked = ::openManga,
|
||||
onContinueReadingClicked = ::continueReading,
|
||||
onGlobalSearchClicked = {
|
||||
router.pushController(GlobalSearchController(presenter.searchQuery))
|
||||
},
|
||||
onChangeCategoryClicked = ::showMangaCategoriesDialog,
|
||||
onMarkAsReadClicked = { markReadStatus(true) },
|
||||
onMarkAsUnreadClicked = { markReadStatus(false) },
|
||||
onDownloadClicked = ::runDownloadChapterAction,
|
||||
onDeleteClicked = ::showDeleteMangaDialog,
|
||||
onClickFilter = ::showSettingsSheet,
|
||||
onClickRefresh = {
|
||||
val started = LibraryUpdateService.start(context, it)
|
||||
context.toast(if (started) R.string.updating_category else R.string.update_already_running)
|
||||
started
|
||||
},
|
||||
onClickOpenRandomManga = {
|
||||
val items = getMangaForCategory.map { it.libraryManga.manga.id }
|
||||
if (getMangaForCategory.isNotEmpty()) {
|
||||
openManga(items.random())
|
||||
} else {
|
||||
context.toast(R.string.information_no_entries_found)
|
||||
}
|
||||
},
|
||||
onClickInvertSelection = { presenter.invertSelection(presenter.activeCategory) },
|
||||
onClickSelectAll = { presenter.selectAll(presenter.activeCategory) },
|
||||
onClickUnselectAll = ::clearSelection,
|
||||
)
|
||||
|
||||
val onDismissRequest = { presenter.dialog = null }
|
||||
when (val dialog = presenter.dialog) {
|
||||
is LibraryPresenter.Dialog.ChangeCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onEditCategories = {
|
||||
presenter.clearSelection()
|
||||
router.pushController(CategoryController())
|
||||
},
|
||||
onConfirm = { include, exclude ->
|
||||
presenter.clearSelection()
|
||||
presenter.setMangaCategories(dialog.manga, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is LibraryPresenter.Dialog.DeleteManga -> {
|
||||
DeleteLibraryMangaDialog(
|
||||
containsLocalManga = dialog.manga.any(Manga::isLocal),
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { deleteManga, deleteChapter ->
|
||||
presenter.removeMangas(dialog.manga, deleteManga, deleteChapter)
|
||||
presenter.clearSelection()
|
||||
},
|
||||
)
|
||||
}
|
||||
is LibraryPresenter.Dialog.DownloadCustomAmount -> {
|
||||
DownloadCustomAmountDialog(
|
||||
maxAmount = dialog.max,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { amount ->
|
||||
presenter.downloadUnreadChapters(dialog.manga, amount)
|
||||
presenter.clearSelection()
|
||||
},
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
|
||||
LaunchedEffect(presenter.selectionMode) {
|
||||
// Could perhaps be removed when navigation is in a Compose world
|
||||
if (router.backstackSize == 1) {
|
||||
(activity as? MainActivity)?.showBottomNav(presenter.selectionMode.not())
|
||||
}
|
||||
}
|
||||
LaunchedEffect(presenter.isLoading) {
|
||||
if (!presenter.isLoading) {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleBack(): Boolean {
|
||||
return when {
|
||||
presenter.selection.isNotEmpty() -> {
|
||||
presenter.clearSelection()
|
||||
true
|
||||
}
|
||||
presenter.searchQuery != null -> {
|
||||
presenter.searchQuery = null
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
Navigator(screen = LibraryScreen)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
settingsSheet = LibrarySettingsSheet(router) { group ->
|
||||
when (group) {
|
||||
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
|
||||
else -> {} // Handled via different mechanisms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isEnter) {
|
||||
presenter.subscribeLibrary()
|
||||
settingsSheet = LibrarySettingsSheet(router)
|
||||
viewScope.launch {
|
||||
LibraryScreen.openSettingsSheetEvent
|
||||
.collectLatest(::showSettingsSheet)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,111 +41,13 @@ class LibraryController(
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
fun showSettingsSheet() {
|
||||
presenter.categories.getOrNull(presenter.activeCategory)?.let { category ->
|
||||
fun showSettingsSheet(category: Category? = null) {
|
||||
if (category != null) {
|
||||
settingsSheet?.show(category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFilterChanged() {
|
||||
viewScope.launchUI {
|
||||
presenter.requestFilterUpdate()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
presenter.searchQuery = query
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val settingsSheet = settingsSheet ?: return
|
||||
presenter.hasActiveFilters = settingsSheet.filters.hasActiveFilters()
|
||||
}
|
||||
|
||||
private fun openManga(mangaId: Long) {
|
||||
presenter.onOpenManga()
|
||||
router.pushController(MangaController(mangaId))
|
||||
}
|
||||
|
||||
private fun continueReading(libraryManga: LibraryManga) {
|
||||
viewScope.launchIO {
|
||||
val chapter = presenter.getNextUnreadChapter(libraryManga.manga)
|
||||
if (chapter != null) {
|
||||
openChapter(chapter)
|
||||
} else {
|
||||
withUIContext { activity?.toast(R.string.no_next_chapter) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openChapter(chapter: Chapter) {
|
||||
activity?.run {
|
||||
startActivity(ReaderActivity.newIntent(this, chapter.mangaId, chapter.id))
|
||||
viewScope.launch { LibraryScreen.requestOpenSettingsSheet() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all of the manga currently selected, and
|
||||
* invalidate the action mode to revert the top toolbar
|
||||
*/
|
||||
private fun clearSelection() {
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selected manga to a list of categories.
|
||||
*/
|
||||
private fun showMangaCategoriesDialog() {
|
||||
viewScope.launchIO {
|
||||
// Create a copy of selected manga
|
||||
val mangaList = presenter.selection.map { it.manga }
|
||||
|
||||
// Hide the default category because it has a different behavior than the ones from db.
|
||||
val categories = presenter.categories.filter { it.id != 0L }
|
||||
|
||||
// Get indexes of the common categories to preselect.
|
||||
val common = presenter.getCommonCategories(mangaList)
|
||||
// Get indexes of the mix categories to preselect.
|
||||
val mix = presenter.getMixCategories(mangaList)
|
||||
val preselected = categories.map {
|
||||
when (it) {
|
||||
in common -> CheckboxState.State.Checked(it)
|
||||
in mix -> CheckboxState.TriState.Exclude(it)
|
||||
else -> CheckboxState.State.None(it)
|
||||
}
|
||||
}
|
||||
presenter.dialog = LibraryPresenter.Dialog.ChangeCategory(mangaList, preselected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runDownloadChapterAction(action: DownloadAction) {
|
||||
val mangas = presenter.selection.map { it.manga }.toList()
|
||||
when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> presenter.downloadUnreadChapters(mangas, 1)
|
||||
DownloadAction.NEXT_5_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 5)
|
||||
DownloadAction.NEXT_10_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 10)
|
||||
DownloadAction.UNREAD_CHAPTERS -> presenter.downloadUnreadChapters(mangas, null)
|
||||
DownloadAction.CUSTOM -> {
|
||||
presenter.dialog = LibraryPresenter.Dialog.DownloadCustomAmount(
|
||||
mangas,
|
||||
presenter.selection.maxOf { it.unreadCount }.toInt(),
|
||||
)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
private fun markReadStatus(read: Boolean) {
|
||||
val mangaList = presenter.selection.toList()
|
||||
presenter.markReadStatus(mangaList.map { it.manga }, read)
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
private fun showDeleteMangaDialog() {
|
||||
val mangaList = presenter.selection.map { it.manga }
|
||||
presenter.dialog = LibraryPresenter.Dialog.DeleteManga(mangaList)
|
||||
}
|
||||
fun search(query: String) = LibraryScreen.search(query)
|
||||
}
|
||||
|
@ -0,0 +1,270 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.util.fastAll
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.library.model.display
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
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.LoadingScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.library.components.LibraryContent
|
||||
import eu.kanade.presentation.library.components.LibraryToolbar
|
||||
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object LibraryScreen : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
val screenModel = rememberScreenModel { LibraryScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val onClickRefresh: (Category?) -> Boolean = {
|
||||
val started = LibraryUpdateService.start(context, it)
|
||||
scope.launch {
|
||||
val msgRes = if (started) R.string.updating_category else R.string.update_already_running
|
||||
snackbarHostState.showSnackbar(context.getString(msgRes))
|
||||
}
|
||||
started
|
||||
}
|
||||
val onClickFilter: () -> Unit = {
|
||||
scope.launch { sendSettingsSheetIntent(state.categories[screenModel.activeCategory]) }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
val title = state.getToolbarTitle(
|
||||
defaultTitle = stringResource(R.string.label_library),
|
||||
defaultCategoryTitle = stringResource(R.string.label_default),
|
||||
page = screenModel.activeCategory,
|
||||
)
|
||||
val tabVisible = state.showCategoryTabs && state.categories.size > 1
|
||||
LibraryToolbar(
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
selectedCount = state.selection.size,
|
||||
title = title,
|
||||
incognitoMode = !tabVisible && screenModel.isIncognitoMode,
|
||||
downloadedOnlyMode = !tabVisible && screenModel.isDownloadOnly,
|
||||
onClickUnselectAll = screenModel::clearSelection,
|
||||
onClickSelectAll = { screenModel.selectAll(screenModel.activeCategory) },
|
||||
onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategory) },
|
||||
onClickFilter = onClickFilter,
|
||||
onClickRefresh = { onClickRefresh(null) },
|
||||
onClickOpenRandomManga = {
|
||||
scope.launch {
|
||||
val randomItem = screenModel.getRandomLibraryItemForCurrentCategory()
|
||||
if (randomItem != null) {
|
||||
router.openManga(randomItem.libraryManga.manga.id)
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.information_no_entries_found))
|
||||
}
|
||||
}
|
||||
},
|
||||
searchQuery = state.searchQuery,
|
||||
onSearchQueryChange = screenModel::search,
|
||||
scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
LibraryBottomActionMenu(
|
||||
visible = state.selectionMode,
|
||||
onChangeCategoryClicked = screenModel::openChangeCategoryDialog,
|
||||
onMarkAsReadClicked = { screenModel.markReadSelection(true) },
|
||||
onMarkAsUnreadClicked = { screenModel.markReadSelection(false) },
|
||||
onDownloadClicked = screenModel::runDownloadActionSelection
|
||||
.takeIf { state.selection.fastAll { !it.manga.isLocal() } },
|
||||
onDeleteClicked = screenModel::openDeleteMangaDialog,
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
|
||||
) { contentPadding ->
|
||||
if (state.isLoading) {
|
||||
LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
if (state.searchQuery.isNullOrEmpty() && state.library.isEmpty()) {
|
||||
val handler = LocalUriHandler.current
|
||||
EmptyScreen(
|
||||
textResource = R.string.information_empty_library,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
actions = listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.getting_started_guide,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
|
||||
),
|
||||
),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LibraryContent(
|
||||
categories = state.categories,
|
||||
searchQuery = state.searchQuery,
|
||||
selection = state.selection,
|
||||
contentPadding = contentPadding,
|
||||
currentPage = { screenModel.activeCategory },
|
||||
isLibraryEmpty = state.library.isEmpty(),
|
||||
showPageTabs = state.showCategoryTabs,
|
||||
onChangeCurrentPage = { screenModel.activeCategory = it },
|
||||
onMangaClicked = { router.openManga(it) },
|
||||
onContinueReadingClicked = { it: LibraryManga ->
|
||||
scope.launchIO {
|
||||
val chapter = screenModel.getNextUnreadChapter(it.manga)
|
||||
if (chapter != null) {
|
||||
context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id))
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter))
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}.takeIf { state.showMangaContinueButton },
|
||||
onToggleSelection = { screenModel.toggleSelection(it) },
|
||||
onToggleRangeSelection = {
|
||||
screenModel.toggleRangeSelection(it)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onRefresh = onClickRefresh,
|
||||
onGlobalSearchClicked = {
|
||||
router.pushController(GlobalSearchController(screenModel.state.value.searchQuery))
|
||||
},
|
||||
getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
|
||||
getDisplayModeForPage = { state.categories[it].display },
|
||||
getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) },
|
||||
getLibraryForPage = { state.getLibraryItemsByPage(it) },
|
||||
isDownloadOnly = screenModel.isDownloadOnly,
|
||||
isIncognitoMode = screenModel.isIncognitoMode,
|
||||
)
|
||||
}
|
||||
|
||||
val onDismissRequest = screenModel::closeDialog
|
||||
when (val dialog = state.dialog) {
|
||||
is LibraryScreenModel.Dialog.ChangeCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onEditCategories = {
|
||||
screenModel.clearSelection()
|
||||
router.pushController(CategoryController())
|
||||
},
|
||||
onConfirm = { include, exclude ->
|
||||
screenModel.clearSelection()
|
||||
screenModel.setMangaCategories(dialog.manga, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is LibraryScreenModel.Dialog.DeleteManga -> {
|
||||
DeleteLibraryMangaDialog(
|
||||
containsLocalManga = dialog.manga.any(Manga::isLocal),
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { deleteManga, deleteChapter ->
|
||||
screenModel.removeMangas(dialog.manga, deleteManga, deleteChapter)
|
||||
screenModel.clearSelection()
|
||||
},
|
||||
)
|
||||
}
|
||||
is LibraryScreenModel.Dialog.DownloadCustomAmount -> {
|
||||
DownloadCustomAmountDialog(
|
||||
maxAmount = dialog.max,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { amount ->
|
||||
screenModel.downloadUnreadChapters(dialog.manga, amount)
|
||||
screenModel.clearSelection()
|
||||
},
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
|
||||
BackHandler(enabled = state.selectionMode || state.searchQuery != null) {
|
||||
when {
|
||||
state.selectionMode -> screenModel.clearSelection()
|
||||
state.searchQuery != null -> screenModel.search(null)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.selectionMode) {
|
||||
// Could perhaps be removed when navigation is in a Compose world
|
||||
if (router.backstackSize == 1) {
|
||||
(context as? MainActivity)?.showBottomNav(!state.selectionMode)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(state.isLoading) {
|
||||
if (!state.isLoading) {
|
||||
(context as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch { queryEvent.collectLatest(screenModel::search) }
|
||||
launch { requestSettingsSheetEvent.collectLatest { onClickFilter() } }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Router.openManga(mangaId: Long) {
|
||||
pushController(MangaController(mangaId))
|
||||
}
|
||||
|
||||
// For invoking search from other screen
|
||||
private val queryEvent = MutableSharedFlow<String>(replay = 1)
|
||||
fun search(query: String) = queryEvent.tryEmit(query)
|
||||
|
||||
// For opening settings sheet in LibraryController
|
||||
private val requestSettingsSheetEvent = MutableSharedFlow<Unit>()
|
||||
private val openSettingsSheetEvent_ = MutableSharedFlow<Category>()
|
||||
val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow()
|
||||
private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category)
|
||||
suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit)
|
||||
}
|
@ -1,18 +1,15 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.CheckboxState
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.util.fastFilter
|
||||
import eu.kanade.core.util.fastFilterNot
|
||||
import eu.kanade.core.util.fastMapNotNull
|
||||
@ -35,11 +32,8 @@ import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.library.LibraryState
|
||||
import eu.kanade.presentation.library.LibraryStateImpl
|
||||
import eu.kanade.presentation.library.components.LibraryToolbarTitle
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
@ -47,38 +41,33 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Class containing library information.
|
||||
*/
|
||||
private data class Library(val categories: List<Category>, val mangaMap: LibraryMap)
|
||||
|
||||
/**
|
||||
* Typealias for the library manga, using the category as keys, and list of manga as values.
|
||||
*/
|
||||
typealias LibraryMap = Map<Long, List<LibraryItem>>
|
||||
typealias LibraryMap = Map<Category, List<LibraryItem>>
|
||||
|
||||
class LibraryPresenter(
|
||||
private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
|
||||
class LibraryScreenModel(
|
||||
private val getLibraryManga: GetLibraryManga = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||
@ -94,90 +83,114 @@ class LibraryPresenter(
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val downloadCache: DownloadCache = Injekt.get(),
|
||||
private val trackManager: TrackManager = Injekt.get(),
|
||||
) : BasePresenter<LibraryController>(), LibraryState by state {
|
||||
) : StateScreenModel<LibraryScreenModel.State>(State()) {
|
||||
|
||||
private var loadedManga by mutableStateOf(emptyMap<Long, List<LibraryItem>>())
|
||||
// This is active category INDEX NUMBER
|
||||
var activeCategory: Int by libraryPreferences.lastUsedCategory().asState(coroutineScope)
|
||||
|
||||
val isLibraryEmpty by derivedStateOf { loadedManga.isEmpty() }
|
||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
|
||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
|
||||
|
||||
val tabVisibility by libraryPreferences.categoryTabs().asState()
|
||||
val mangaCountVisibility by libraryPreferences.categoryNumberOfItems().asState()
|
||||
|
||||
val showDownloadBadges by libraryPreferences.downloadBadge().asState()
|
||||
val showUnreadBadges by libraryPreferences.unreadBadge().asState()
|
||||
val showLocalBadges by libraryPreferences.localBadge().asState()
|
||||
val showLanguageBadges by libraryPreferences.languageBadge().asState()
|
||||
|
||||
var activeCategory: Int by libraryPreferences.lastUsedCategory().asState()
|
||||
|
||||
val showContinueReadingButton by libraryPreferences.showContinueReadingButton().asState()
|
||||
|
||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
||||
|
||||
private val _filterChanges: Channel<Unit> = Channel(Int.MAX_VALUE)
|
||||
private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) }
|
||||
|
||||
private var librarySubscription: Job? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
subscribeLibrary()
|
||||
init {
|
||||
coroutineScope.launchIO {
|
||||
combine(
|
||||
state.map { it.searchQuery }.distinctUntilChanged(),
|
||||
getLibraryFlow(),
|
||||
getTracksPerManga.subscribe(),
|
||||
getTrackingFilterFlow(),
|
||||
) { searchQuery, library, tracks, loggedInTrackServices ->
|
||||
library
|
||||
.applyFilters(tracks, loggedInTrackServices)
|
||||
.applySort()
|
||||
.mapValues { (_, value) ->
|
||||
if (searchQuery != null) {
|
||||
// Filter query
|
||||
value.filter { it.matches(searchQuery) }
|
||||
} else {
|
||||
// Don't do anything
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeLibrary() {
|
||||
/**
|
||||
* TODO:
|
||||
* - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library
|
||||
* - Create new db view and new query to just fetch the current category save as needed to instance variable
|
||||
* - Fetch badges to maps and retrieve as needed instead of fetching all of them at once
|
||||
*/
|
||||
if (librarySubscription == null || librarySubscription!!.isCancelled) {
|
||||
librarySubscription = presenterScope.launchIO {
|
||||
combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ ->
|
||||
library.mangaMap
|
||||
.applyFilters(tracks)
|
||||
.applySort(library.categories)
|
||||
}
|
||||
.collectLatest {
|
||||
state.isLoading = false
|
||||
loadedManga = it
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
library = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combine(
|
||||
libraryPreferences.categoryTabs().changes(),
|
||||
libraryPreferences.categoryNumberOfItems().changes(),
|
||||
libraryPreferences.showContinueReadingButton().changes(),
|
||||
) { a, b, c -> arrayOf(a, b, c) }
|
||||
.onEach { (showCategoryTabs, showMangaCount, showMangaContinueButton) ->
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
showCategoryTabs = showCategoryTabs,
|
||||
showMangaCount = showMangaCount,
|
||||
showMangaContinueButton = showMangaContinueButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
combine(
|
||||
getLibraryItemPreferencesFlow(),
|
||||
getTrackingFilterFlow(),
|
||||
) { prefs, trackFilter ->
|
||||
val a = (
|
||||
prefs.filterDownloaded or
|
||||
prefs.filterUnread or
|
||||
prefs.filterStarted or
|
||||
prefs.filterBookmarked or
|
||||
prefs.filterCompleted
|
||||
) != TriStateGroup.State.IGNORE.value
|
||||
val b = trackFilter.values.any { it != TriStateGroup.State.IGNORE.value }
|
||||
a || b
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
mutableState.update { state ->
|
||||
state.copy(hasActiveFilters = it)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*/
|
||||
private fun LibraryMap.applyFilters(trackMap: Map<Long, List<Long>>): LibraryMap {
|
||||
val downloadedOnly = preferences.downloadedOnly().get()
|
||||
val filterDownloaded = libraryPreferences.filterDownloaded().get()
|
||||
val filterUnread = libraryPreferences.filterUnread().get()
|
||||
val filterStarted = libraryPreferences.filterStarted().get()
|
||||
val filterBookmarked = libraryPreferences.filterBookmarked().get()
|
||||
val filterCompleted = libraryPreferences.filterCompleted().get()
|
||||
private suspend fun LibraryMap.applyFilters(
|
||||
trackMap: Map<Long, List<Long>>,
|
||||
loggedInTrackServices: Map<Long, Int>,
|
||||
): LibraryMap {
|
||||
val prefs = getLibraryItemPreferencesFlow().first()
|
||||
val downloadedOnly = prefs.globalFilterDownloaded
|
||||
val filterDownloaded = prefs.filterDownloaded
|
||||
val filterUnread = prefs.filterUnread
|
||||
val filterStarted = prefs.filterStarted
|
||||
val filterBookmarked = prefs.filterBookmarked
|
||||
val filterCompleted = prefs.filterCompleted
|
||||
|
||||
val loggedInTrackServices = trackManager.services.fastFilter { trackService -> trackService.isLogged }
|
||||
.associate { trackService ->
|
||||
trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get()
|
||||
}
|
||||
val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
|
||||
|
||||
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.EXCLUDE.value) it.key else null }
|
||||
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.INCLUDE.value) it.key else null }
|
||||
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.EXCLUDE.value) it.key else null }
|
||||
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.INCLUDE.value) it.key else null }
|
||||
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
|
||||
|
||||
val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
|
||||
if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true
|
||||
if (!downloadedOnly && filterDownloaded == TriStateGroup.State.IGNORE.value) return@downloaded true
|
||||
val isDownloaded = when {
|
||||
item.libraryManga.manga.isLocal() -> true
|
||||
item.downloadCount != -1L -> item.downloadCount > 0
|
||||
else -> downloadManager.getDownloadCount(item.libraryManga.manga) > 0
|
||||
}
|
||||
|
||||
return@downloaded if (downloadedOnly || filterDownloaded == State.INCLUDE.value) {
|
||||
return@downloaded if (downloadedOnly || filterDownloaded == TriStateGroup.State.INCLUDE.value) {
|
||||
isDownloaded
|
||||
} else {
|
||||
!isDownloaded
|
||||
@ -185,10 +198,10 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
val filterFnUnread: (LibraryItem) -> Boolean = unread@{ item ->
|
||||
if (filterUnread == State.IGNORE.value) return@unread true
|
||||
if (filterUnread == TriStateGroup.State.IGNORE.value) return@unread true
|
||||
val isUnread = item.libraryManga.unreadCount > 0
|
||||
|
||||
return@unread if (filterUnread == State.INCLUDE.value) {
|
||||
return@unread if (filterUnread == TriStateGroup.State.INCLUDE.value) {
|
||||
isUnread
|
||||
} else {
|
||||
!isUnread
|
||||
@ -196,10 +209,10 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
val filterFnStarted: (LibraryItem) -> Boolean = started@{ item ->
|
||||
if (filterStarted == State.IGNORE.value) return@started true
|
||||
if (filterStarted == TriStateGroup.State.IGNORE.value) return@started true
|
||||
val hasStarted = item.libraryManga.hasStarted
|
||||
|
||||
return@started if (filterStarted == State.INCLUDE.value) {
|
||||
return@started if (filterStarted == TriStateGroup.State.INCLUDE.value) {
|
||||
hasStarted
|
||||
} else {
|
||||
!hasStarted
|
||||
@ -207,11 +220,11 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
val filterFnBookmarked: (LibraryItem) -> Boolean = bookmarked@{ item ->
|
||||
if (filterBookmarked == State.IGNORE.value) return@bookmarked true
|
||||
if (filterBookmarked == TriStateGroup.State.IGNORE.value) return@bookmarked true
|
||||
|
||||
val hasBookmarks = item.libraryManga.hasBookmarks
|
||||
|
||||
return@bookmarked if (filterBookmarked == State.INCLUDE.value) {
|
||||
return@bookmarked if (filterBookmarked == TriStateGroup.State.INCLUDE.value) {
|
||||
hasBookmarks
|
||||
} else {
|
||||
!hasBookmarks
|
||||
@ -219,10 +232,10 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
val filterFnCompleted: (LibraryItem) -> Boolean = completed@{ item ->
|
||||
if (filterCompleted == State.IGNORE.value) return@completed true
|
||||
if (filterCompleted == TriStateGroup.State.IGNORE.value) return@completed true
|
||||
val isCompleted = item.libraryManga.manga.status.toInt() == SManga.COMPLETED
|
||||
|
||||
return@completed if (filterCompleted == State.INCLUDE.value) {
|
||||
return@completed if (filterCompleted == TriStateGroup.State.INCLUDE.value) {
|
||||
isCompleted
|
||||
} else {
|
||||
!isCompleted
|
||||
@ -266,9 +279,7 @@ class LibraryPresenter(
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*/
|
||||
private fun LibraryMap.applySort(categories: List<Category>): LibraryMap {
|
||||
val sortModes = categories.associate { it.id to it.sort }
|
||||
|
||||
private fun LibraryMap.applySort(): LibraryMap {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
@ -278,7 +289,7 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
val sort = sortModes[i1.libraryManga.category]!!
|
||||
val sort = keys.find { it.id == i1.libraryManga.category }!!.sort
|
||||
when (sort.type) {
|
||||
LibrarySort.Type.Alphabetical -> {
|
||||
sortAlphabetically(i1, i2)
|
||||
@ -308,12 +319,11 @@ class LibraryPresenter(
|
||||
LibrarySort.Type.DateAdded -> {
|
||||
i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded)
|
||||
}
|
||||
else -> throw IllegalStateException("Invalid SortModeSetting: ${sort.type}")
|
||||
}
|
||||
}
|
||||
|
||||
return this.mapValues { entry ->
|
||||
val comparator = if (sortModes[entry.key]!!.isAscending) {
|
||||
val comparator = if (keys.find { it.id == entry.key.id }!!.sort.isAscending) {
|
||||
Comparator(sortFn)
|
||||
} else {
|
||||
Collections.reverseOrder(sortFn)
|
||||
@ -323,24 +333,52 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLibraryItemPreferencesFlow(): Flow<ItemPreferences> {
|
||||
return combine(
|
||||
libraryPreferences.downloadBadge().changes(),
|
||||
libraryPreferences.unreadBadge().changes(),
|
||||
libraryPreferences.localBadge().changes(),
|
||||
libraryPreferences.languageBadge().changes(),
|
||||
|
||||
preferences.downloadedOnly().changes(),
|
||||
libraryPreferences.filterDownloaded().changes(),
|
||||
libraryPreferences.filterUnread().changes(),
|
||||
libraryPreferences.filterStarted().changes(),
|
||||
libraryPreferences.filterBookmarked().changes(),
|
||||
libraryPreferences.filterCompleted().changes(),
|
||||
transform = {
|
||||
ItemPreferences(
|
||||
downloadBadge = it[0] as Boolean,
|
||||
unreadBadge = it[1] as Boolean,
|
||||
localBadge = it[2] as Boolean,
|
||||
languageBadge = it[3] as Boolean,
|
||||
globalFilterDownloaded = it[4] as Boolean,
|
||||
filterDownloaded = it[5] as Int,
|
||||
filterUnread = it[6] as Int,
|
||||
filterStarted = it[7] as Int,
|
||||
filterBookmarked = it[8] as Int,
|
||||
filterCompleted = it[9] as Int,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories and all its manga from the database.
|
||||
*
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
private fun getLibraryFlow(): Flow<Library> {
|
||||
private fun getLibraryFlow(): Flow<LibraryMap> {
|
||||
val libraryMangasFlow = combine(
|
||||
getLibraryManga.subscribe(),
|
||||
libraryPreferences.downloadBadge().changes(),
|
||||
libraryPreferences.filterDownloaded().changes(),
|
||||
preferences.downloadedOnly().changes(),
|
||||
getLibraryItemPreferencesFlow(),
|
||||
downloadCache.changes,
|
||||
) { libraryMangaList, downloadBadgePref, filterDownloadedPref, downloadedOnly, _ ->
|
||||
) { libraryMangaList, prefs, _ ->
|
||||
libraryMangaList
|
||||
.map { libraryManga ->
|
||||
val needsDownloadCounts = downloadBadgePref ||
|
||||
filterDownloadedPref != State.IGNORE.value ||
|
||||
downloadedOnly
|
||||
val needsDownloadCounts = prefs.downloadBadge ||
|
||||
prefs.filterDownloaded != TriStateGroup.State.IGNORE.value ||
|
||||
prefs.globalFilterDownloaded
|
||||
|
||||
// Display mode based on user preference: take it from global library setting or category
|
||||
LibraryItem(libraryManga).apply {
|
||||
@ -349,39 +387,44 @@ class LibraryPresenter(
|
||||
} else {
|
||||
0
|
||||
}
|
||||
unreadCount = libraryManga.unreadCount
|
||||
isLocal = libraryManga.manga.isLocal()
|
||||
sourceLanguage = sourceManager.getOrStub(libraryManga.manga.source).lang
|
||||
unreadCount = if (prefs.unreadBadge) libraryManga.unreadCount else 0
|
||||
isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false
|
||||
sourceLanguage = if (prefs.languageBadge) {
|
||||
sourceManager.getOrStub(libraryManga.manga.source).lang
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
.groupBy { it.libraryManga.category }
|
||||
}
|
||||
|
||||
return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga ->
|
||||
val displayCategories = if (libraryManga.isNotEmpty() && libraryManga.containsKey(0).not()) {
|
||||
val displayCategories = if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) {
|
||||
categories.fastFilterNot { it.isSystemCategory }
|
||||
} else {
|
||||
categories
|
||||
}
|
||||
|
||||
state.categories = displayCategories
|
||||
Library(categories, libraryManga)
|
||||
displayCategories.associateWith { libraryManga[it.id] ?: emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be filtered.
|
||||
* Flow of tracking filter preferences
|
||||
*
|
||||
* @return map of track id with the filter value
|
||||
*/
|
||||
suspend fun requestFilterUpdate() = withIOContext {
|
||||
_filterChanges.send(Unit)
|
||||
private fun getTrackingFilterFlow(): Flow<Map<Long, Int>> {
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val a = loggedServices
|
||||
.map { libraryPreferences.filterTracking(it.id.toInt()).changes() }
|
||||
.toTypedArray()
|
||||
return combine(*a) {
|
||||
loggedServices
|
||||
.mapIndexed { index, trackService -> trackService.id to it[index] }
|
||||
.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is opened.
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
librarySubscription?.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,7 +432,7 @@ class LibraryPresenter(
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas
|
||||
.map { getCategories.await(it.id).toSet() }
|
||||
@ -405,13 +448,37 @@ class LibraryPresenter(
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
|
||||
private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
|
||||
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
|
||||
return mangaCategories.flatten().distinct().subtract(common)
|
||||
}
|
||||
|
||||
fun runDownloadActionSelection(action: DownloadAction) {
|
||||
val selection = state.value.selection
|
||||
val mangas = selection.map { it.manga }.toList()
|
||||
when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> downloadUnreadChapters(mangas, 1)
|
||||
DownloadAction.NEXT_5_CHAPTERS -> downloadUnreadChapters(mangas, 5)
|
||||
DownloadAction.NEXT_10_CHAPTERS -> downloadUnreadChapters(mangas, 10)
|
||||
DownloadAction.UNREAD_CHAPTERS -> downloadUnreadChapters(mangas, null)
|
||||
DownloadAction.CUSTOM -> {
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
dialog = Dialog.DownloadCustomAmount(
|
||||
mangas,
|
||||
selection.maxOf { it.unreadCount }.toInt(),
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the amount specified of unread chapters from the list of mangas given.
|
||||
*
|
||||
@ -419,7 +486,7 @@ class LibraryPresenter(
|
||||
* @param amount the amount to queue or null to queue all
|
||||
*/
|
||||
fun downloadUnreadChapters(mangas: List<Manga>, amount: Int?) {
|
||||
presenterScope.launchNonCancellable {
|
||||
coroutineScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
val chapters = getNextChapters.await(manga.id)
|
||||
.fastFilterNot { chapter ->
|
||||
@ -440,18 +507,18 @@ class LibraryPresenter(
|
||||
|
||||
/**
|
||||
* Marks mangas' chapters read status.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
|
||||
presenterScope.launchNonCancellable {
|
||||
fun markReadSelection(read: Boolean) {
|
||||
val mangas = state.value.selection.toList()
|
||||
coroutineScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
setReadStatus.await(
|
||||
manga = manga,
|
||||
manga = manga.manga,
|
||||
read = read,
|
||||
)
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -462,7 +529,7 @@ class LibraryPresenter(
|
||||
* @param deleteChapters whether to delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangas(mangaList: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
|
||||
presenterScope.launchNonCancellable {
|
||||
coroutineScope.launchNonCancellable {
|
||||
val mangaToDelete = mangaList.distinctBy { it.id }
|
||||
|
||||
if (deleteFromLibrary) {
|
||||
@ -495,7 +562,7 @@ class LibraryPresenter(
|
||||
* @param removeCategories the categories to remove in all mangas.
|
||||
*/
|
||||
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
|
||||
presenterScope.launchNonCancellable {
|
||||
coroutineScope.launchNonCancellable {
|
||||
mangaList.forEach { manga ->
|
||||
val categoryIds = getCategories.await(manga.id)
|
||||
.map { it.id }
|
||||
@ -508,87 +575,33 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaCountForCategory(categoryId: Long): androidx.compose.runtime.State<Int?> {
|
||||
return produceState<Int?>(initialValue = null, loadedManga) {
|
||||
value = loadedManga[categoryId]?.size
|
||||
}
|
||||
}
|
||||
|
||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||
return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState()
|
||||
return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState(coroutineScope)
|
||||
}
|
||||
|
||||
// TODO: This is good but should we separate title from count or get categories with count from db
|
||||
@Composable
|
||||
fun getToolbarTitle(): androidx.compose.runtime.State<LibraryToolbarTitle> {
|
||||
val category = categories.getOrNull(activeCategory)
|
||||
|
||||
val defaultTitle = stringResource(R.string.label_library)
|
||||
val categoryName = category?.visualName ?: defaultTitle
|
||||
|
||||
val default = remember { LibraryToolbarTitle(defaultTitle) }
|
||||
|
||||
return produceState(initialValue = default, category, loadedManga, mangaCountVisibility, tabVisibility) {
|
||||
val title = if (tabVisibility.not()) categoryName else defaultTitle
|
||||
val count = when {
|
||||
category == null || mangaCountVisibility.not() -> null
|
||||
tabVisibility.not() -> loadedManga[category.id]?.size
|
||||
else -> loadedManga.values.flatten().distinctBy { it.libraryManga.manga.id }.size
|
||||
}
|
||||
|
||||
value = when (category) {
|
||||
null -> default
|
||||
else -> LibraryToolbarTitle(title, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaForCategory(page: Int): List<LibraryItem> {
|
||||
val categoryId = remember(categories, page) {
|
||||
categories.getOrNull(page)?.id ?: -1
|
||||
}
|
||||
val unfiltered = remember(loadedManga, categoryId) {
|
||||
loadedManga[categoryId] ?: emptyList()
|
||||
}
|
||||
return remember(unfiltered, searchQuery) {
|
||||
if (searchQuery.isNullOrBlank()) {
|
||||
queriedMangaMap.clear()
|
||||
unfiltered
|
||||
} else {
|
||||
unfiltered.fastFilter { it.matches(searchQuery!!) }
|
||||
.also { queriedMangaMap[categoryId] = it }
|
||||
}
|
||||
suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
|
||||
return withIOContext {
|
||||
state.value
|
||||
.getLibraryItemsByCategoryId(activeCategory.toLong())
|
||||
.randomOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
state.selection = emptyList()
|
||||
mutableState.update { it.copy(selection = emptyList()) }
|
||||
}
|
||||
|
||||
fun toggleSelection(manga: LibraryManga) {
|
||||
state.selection = selection.toMutableList().apply {
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.toMutableList().apply {
|
||||
if (fastAny { it.id == manga.id }) {
|
||||
removeAll { it.id == manga.id }
|
||||
} else {
|
||||
add(manga)
|
||||
}
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Map is cleared out via [getMangaForCategory] when [searchQuery] is null or blank
|
||||
*/
|
||||
private val queriedMangaMap: MutableMap<Long, List<LibraryItem>> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Used by select all, inverse and range selection.
|
||||
*
|
||||
* If current query is empty then we get manga list from [loadedManga] otherwise from [queriedMangaMap]
|
||||
*/
|
||||
private fun getMangaForCategoryWithQuery(categoryId: Long, query: String?): List<LibraryItem> {
|
||||
return if (query.isNullOrBlank()) loadedManga[categoryId].orEmpty() else queriedMangaMap[categoryId].orEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -596,14 +609,15 @@ class LibraryPresenter(
|
||||
* same category as the given manga
|
||||
*/
|
||||
fun toggleRangeSelection(manga: LibraryManga) {
|
||||
state.selection = selection.toMutableList().apply {
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.toMutableList().apply {
|
||||
val lastSelected = lastOrNull()
|
||||
if (lastSelected?.category != manga.category) {
|
||||
add(manga)
|
||||
return@apply
|
||||
}
|
||||
|
||||
val items = getMangaForCategoryWithQuery(manga.category, searchQuery)
|
||||
val items = state.getLibraryItemsByCategoryId(manga.category)
|
||||
.fastMap { it.libraryManga }
|
||||
val lastMangaIndex = items.indexOf(lastSelected)
|
||||
val curMangaIndex = items.indexOf(manga)
|
||||
@ -620,31 +634,75 @@ class LibraryPresenter(
|
||||
}
|
||||
addAll(newSelections)
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll(index: Int) {
|
||||
state.selection = state.selection.toMutableList().apply {
|
||||
val categoryId = categories.getOrNull(index)?.id ?: -1
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.toMutableList().apply {
|
||||
val categoryId = state.categories.getOrNull(index)?.id ?: -1
|
||||
val selectedIds = fastMap { it.id }
|
||||
val newSelections = getMangaForCategoryWithQuery(categoryId, searchQuery)
|
||||
val newSelections = state.getLibraryItemsByCategoryId(categoryId)
|
||||
.fastMapNotNull { item ->
|
||||
item.libraryManga.takeUnless { it.id in selectedIds }
|
||||
}
|
||||
|
||||
addAll(newSelections)
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection(index: Int) {
|
||||
state.selection = selection.toMutableList().apply {
|
||||
val categoryId = categories[index].id
|
||||
val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga }
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.toMutableList().apply {
|
||||
val categoryId = state.categories[index].id
|
||||
val items = state.getLibraryItemsByCategoryId(categoryId).fastMap { it.libraryManga }
|
||||
val selectedIds = fastMap { it.id }
|
||||
val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds }
|
||||
val toRemoveIds = toRemove.fastMap { it.id }
|
||||
removeAll { it.id in toRemoveIds }
|
||||
addAll(toAdd)
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
mutableState.update { it.copy(searchQuery = query) }
|
||||
}
|
||||
|
||||
fun openChangeCategoryDialog() {
|
||||
coroutineScope.launchIO {
|
||||
// Create a copy of selected manga
|
||||
val mangaList = state.value.selection.map { it.manga }
|
||||
|
||||
// Hide the default category because it has a different behavior than the ones from db.
|
||||
val categories = state.value.categories.filter { it.id != 0L }
|
||||
|
||||
// Get indexes of the common categories to preselect.
|
||||
val common = getCommonCategories(mangaList)
|
||||
// Get indexes of the mix categories to preselect.
|
||||
val mix = getMixCategories(mangaList)
|
||||
val preselected = categories.map {
|
||||
when (it) {
|
||||
in common -> CheckboxState.State.Checked(it)
|
||||
in mix -> CheckboxState.TriState.Exclude(it)
|
||||
else -> CheckboxState.State.None(it)
|
||||
}
|
||||
}
|
||||
mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun openDeleteMangaDialog() {
|
||||
val mangaList = state.value.selection.map { it.manga }
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteManga(mangaList)) }
|
||||
}
|
||||
|
||||
fun closeDialog() {
|
||||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
@ -652,4 +710,80 @@ class LibraryPresenter(
|
||||
data class DeleteManga(val manga: List<Manga>) : Dialog()
|
||||
data class DownloadCustomAmount(val manga: List<Manga>, val max: Int) : Dialog()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class ItemPreferences(
|
||||
val downloadBadge: Boolean,
|
||||
val unreadBadge: Boolean,
|
||||
val localBadge: Boolean,
|
||||
val languageBadge: Boolean,
|
||||
|
||||
val globalFilterDownloaded: Boolean,
|
||||
val filterDownloaded: Int,
|
||||
val filterUnread: Int,
|
||||
val filterStarted: Int,
|
||||
val filterBookmarked: Int,
|
||||
val filterCompleted: Int,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val isLoading: Boolean = true,
|
||||
val library: LibraryMap = emptyMap(),
|
||||
val searchQuery: String? = null,
|
||||
val selection: List<LibraryManga> = emptyList(),
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val showCategoryTabs: Boolean = false,
|
||||
val showMangaCount: Boolean = false,
|
||||
val showMangaContinueButton: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
val selectionMode = selection.isNotEmpty()
|
||||
|
||||
val categories = library.keys.toList()
|
||||
|
||||
val libraryCount by lazy {
|
||||
library
|
||||
.flatMap { (_, v) -> v }
|
||||
.distinctBy { it.libraryManga.manga.id }
|
||||
.size
|
||||
}
|
||||
|
||||
fun getLibraryItemsByCategoryId(categoryId: Long): List<LibraryItem> {
|
||||
return library.firstNotNullOf { (k, v) -> v.takeIf { k.id == categoryId } }
|
||||
}
|
||||
|
||||
fun getLibraryItemsByPage(page: Int): List<LibraryItem> {
|
||||
return library.values.toTypedArray().getOrNull(page) ?: emptyList()
|
||||
}
|
||||
|
||||
fun getMangaCountForCategory(category: Category): Int? {
|
||||
return library[category]?.size?.takeIf { showMangaCount }
|
||||
}
|
||||
|
||||
fun getToolbarTitle(
|
||||
defaultTitle: String,
|
||||
defaultCategoryTitle: String,
|
||||
page: Int,
|
||||
): LibraryToolbarTitle {
|
||||
val category = categories.getOrNull(page) ?: return LibraryToolbarTitle(defaultTitle)
|
||||
val categoryName = category.let {
|
||||
if (it.isSystemCategory) {
|
||||
defaultCategoryTitle
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
|
||||
val title = if (showCategoryTabs) defaultTitle else categoryName
|
||||
val count = when {
|
||||
!showMangaCount -> null
|
||||
!showCategoryTabs -> getMangaCountForCategory(category)
|
||||
// Whole library count
|
||||
else -> libraryCount
|
||||
}
|
||||
|
||||
return LibraryToolbarTitle(title, count)
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,6 @@ class LibrarySettingsSheet(
|
||||
private val trackManager: TrackManager = Injekt.get(),
|
||||
private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
|
||||
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
|
||||
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
|
||||
) : TabbedBottomSheetDialog(router.activity!!) {
|
||||
|
||||
val filters: Filter
|
||||
@ -43,13 +42,8 @@ class LibrarySettingsSheet(
|
||||
|
||||
init {
|
||||
filters = Filter(router.activity!!)
|
||||
filters.onGroupClicked = onGroupClickListener
|
||||
|
||||
sort = Sort(router.activity!!)
|
||||
sort.onGroupClicked = onGroupClickListener
|
||||
|
||||
display = Display(router.activity!!)
|
||||
display.onGroupClicked = onGroupClickListener
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user