Use Compose for Library list and grid (#7520)
(cherry picked from commit 905c96922bc7059e99c4c7bd89775747d02028a9) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt # app/src/main/res/layout/library_category.xml # app/src/main/res/layout/library_grid_recycler.xml # app/src/main/res/layout/library_list_recycler.xml
This commit is contained in:
parent
6df1a0f79e
commit
b4adab5eb4
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BadgeGroup(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = RoundedCornerShape(4.dp),
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier.clip(shape)) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Badge(
|
||||||
|
text: String,
|
||||||
|
color: Color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
shape: Shape = RectangleShape,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color)
|
||||||
|
.clip(shape),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.util.plus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyLibraryGrid(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
columns: Int,
|
||||||
|
content: LazyGridScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
modifier = modifier,
|
||||||
|
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
|
||||||
|
contentPadding = PaddingValues(8.dp) + WindowInsets.navigationBars.asPaddingValues(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryComfortableGrid(
|
||||||
|
items: List<LibraryItem>,
|
||||||
|
columns: Int,
|
||||||
|
selection: List<LibraryManga>,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
LazyLibraryGrid(
|
||||||
|
columns = columns,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryComfortableGridItem(
|
||||||
|
libraryItem,
|
||||||
|
libraryItem.manga in selection,
|
||||||
|
onClick,
|
||||||
|
onLongClick,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryComfortableGridItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
val manga = item.manga
|
||||||
|
LibraryGridItemSelectable(isSelected = isSelected) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
onClick(manga)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick(manga)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
LibraryGridCover(
|
||||||
|
mangaCover = MangaCover(
|
||||||
|
manga.id!!,
|
||||||
|
manga.source,
|
||||||
|
manga.favorite,
|
||||||
|
manga.thumbnail_url,
|
||||||
|
manga.cover_last_modified,
|
||||||
|
),
|
||||||
|
downloadCount = item.downloadCount,
|
||||||
|
unreadCount = item.unreadCount,
|
||||||
|
isLocal = item.isLocal,
|
||||||
|
language = item.sourceLanguage,
|
||||||
|
// SY -->
|
||||||
|
showPlayButton = item.startReadingButton,
|
||||||
|
onOpenReader = {
|
||||||
|
onOpenReader(manga)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
maxLines = 2,
|
||||||
|
style = LocalTextStyle.current.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryCompactGrid(
|
||||||
|
items: List<LibraryItem>,
|
||||||
|
columns: Int,
|
||||||
|
selection: List<LibraryManga>,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
LazyLibraryGrid(
|
||||||
|
columns = columns,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryCompactGridItem(
|
||||||
|
item = libraryItem,
|
||||||
|
isSelected = libraryItem.manga in selection,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader = onOpenReader,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryCompactGridItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
val manga = item.manga
|
||||||
|
LibraryGridCover(
|
||||||
|
modifier = Modifier
|
||||||
|
.selectedOutline(isSelected)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
onClick(manga)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick(manga)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
mangaCover = eu.kanade.domain.manga.model.MangaCover(
|
||||||
|
manga.id!!,
|
||||||
|
manga.source,
|
||||||
|
manga.favorite,
|
||||||
|
manga.thumbnail_url,
|
||||||
|
manga.cover_last_modified,
|
||||||
|
),
|
||||||
|
downloadCount = item.downloadCount,
|
||||||
|
unreadCount = item.unreadCount,
|
||||||
|
isLocal = item.isLocal,
|
||||||
|
language = item.sourceLanguage,
|
||||||
|
// SY -->
|
||||||
|
showPlayButton = item.startReadingButton,
|
||||||
|
playButtonPosition = PlayButtonPosition.Top,
|
||||||
|
onOpenReader = {
|
||||||
|
onOpenReader(manga)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
0f to Color.Transparent,
|
||||||
|
1f to Color(0xAA000000),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.fillMaxHeight(0.33f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.BottomStart),
|
||||||
|
maxLines = 2,
|
||||||
|
style = LocalTextStyle.current.copy(color = Color.White, fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryCoverOnlyGrid(
|
||||||
|
items: List<LibraryItem>,
|
||||||
|
columns: Int,
|
||||||
|
selection: List<LibraryManga>,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
LazyLibraryGrid(
|
||||||
|
columns = columns,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryCoverOnlyGridItem(
|
||||||
|
item = libraryItem,
|
||||||
|
isSelected = libraryItem.manga in selection,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader = onOpenReader,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryCoverOnlyGridItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
val manga = item.manga
|
||||||
|
LibraryGridCover(
|
||||||
|
modifier = Modifier
|
||||||
|
.selectedOutline(isSelected)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
onClick(manga)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick(manga)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
mangaCover = eu.kanade.domain.manga.model.MangaCover(
|
||||||
|
manga.id!!,
|
||||||
|
manga.source,
|
||||||
|
manga.favorite,
|
||||||
|
manga.thumbnail_url,
|
||||||
|
manga.cover_last_modified,
|
||||||
|
),
|
||||||
|
downloadCount = item.downloadCount,
|
||||||
|
unreadCount = item.unreadCount,
|
||||||
|
isLocal = item.isLocal,
|
||||||
|
language = item.sourceLanguage,
|
||||||
|
// SY -->
|
||||||
|
showPlayButton = item.startReadingButton,
|
||||||
|
onOpenReader = {
|
||||||
|
onOpenReader(manga)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.components.MangaCover
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryGridCover(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
mangaCover: eu.kanade.domain.manga.model.MangaCover,
|
||||||
|
downloadCount: Int,
|
||||||
|
unreadCount: Int,
|
||||||
|
isLocal: Boolean,
|
||||||
|
language: String,
|
||||||
|
// SY -->
|
||||||
|
showPlayButton: Boolean,
|
||||||
|
playButtonPosition: PlayButtonPosition = PlayButtonPosition.Bottom,
|
||||||
|
onOpenReader: () -> Unit,
|
||||||
|
// SY <--
|
||||||
|
content: @Composable BoxScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(MangaCover.Book.ratio),
|
||||||
|
) {
|
||||||
|
MangaCover.Book(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
data = mangaCover,
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
BadgeGroup(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.align(Alignment.TopStart),
|
||||||
|
) {
|
||||||
|
if (downloadCount > 0) {
|
||||||
|
Badge(
|
||||||
|
text = "$downloadCount",
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
Badge(text = "$unreadCount")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY -->
|
||||||
|
Column(
|
||||||
|
Modifier.align(Alignment.TopEnd),
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
) {
|
||||||
|
// SY <--
|
||||||
|
BadgeGroup(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp),
|
||||||
|
) {
|
||||||
|
if (isLocal) {
|
||||||
|
Badge(
|
||||||
|
text = stringResource(id = R.string.local_source_badge),
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isLocal.not() && language.isNotEmpty()) {
|
||||||
|
Badge(
|
||||||
|
text = language,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY -->
|
||||||
|
if (showPlayButton && playButtonPosition == PlayButtonPosition.Top) {
|
||||||
|
StartReadingButton(onOpenReader = onOpenReader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showPlayButton && playButtonPosition == PlayButtonPosition.Bottom) {
|
||||||
|
StartReadingButton(
|
||||||
|
Modifier.align(Alignment.BottomEnd),
|
||||||
|
onOpenReader = onOpenReader,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PlayButtonPosition { Top, Bottom }
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
fun Modifier.selectedOutline(isSelected: Boolean) = composed {
|
||||||
|
val secondary = MaterialTheme.colorScheme.secondary
|
||||||
|
if (isSelected) {
|
||||||
|
drawBehind {
|
||||||
|
val additional = 24.dp.value
|
||||||
|
val offset = additional / 2
|
||||||
|
val height = size.height + additional
|
||||||
|
val width = size.width + additional
|
||||||
|
drawRoundRect(
|
||||||
|
color = secondary,
|
||||||
|
topLeft = Offset(-offset, -offset),
|
||||||
|
size = Size(width, height),
|
||||||
|
cornerRadius = CornerRadius(offset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryGridItemSelectable(
|
||||||
|
isSelected: Boolean,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(Modifier.selectedOutline(isSelected)) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides if (isSelected) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.onBackground) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
|
import eu.kanade.presentation.util.horizontalPadding
|
||||||
|
import eu.kanade.presentation.util.selectedBackground
|
||||||
|
import eu.kanade.presentation.util.verticalPadding
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryList(
|
||||||
|
items: List<LibraryItem>,
|
||||||
|
columns: Int,
|
||||||
|
selection: List<LibraryManga>,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryListItem(
|
||||||
|
item = libraryItem,
|
||||||
|
isSelected = libraryItem.manga in selection,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryListItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
val manga = item.manga
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.selectedBackground(isSelected)
|
||||||
|
.height(56.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick(manga) },
|
||||||
|
onLongClick = { onLongClick(manga) },
|
||||||
|
)
|
||||||
|
.padding(horizontal = horizontalPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
eu.kanade.presentation.components.MangaCover.Square(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = verticalPadding)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
data = MangaCover(
|
||||||
|
manga.id!!,
|
||||||
|
manga.source,
|
||||||
|
manga.favorite,
|
||||||
|
manga.thumbnail_url,
|
||||||
|
manga.cover_last_modified,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = horizontalPadding)
|
||||||
|
.weight(1f),
|
||||||
|
maxLines = 2,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
BadgeGroup {
|
||||||
|
if (item.downloadCount > 0) {
|
||||||
|
Badge(
|
||||||
|
text = "${item.downloadCount}",
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.unreadCount > 0) {
|
||||||
|
Badge(text = "${item.unreadCount}")
|
||||||
|
}
|
||||||
|
if (item.isLocal) {
|
||||||
|
Badge(
|
||||||
|
text = stringResource(id = R.string.local_source_badge),
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) {
|
||||||
|
Badge(
|
||||||
|
text = item.sourceLanguage,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StartReadingButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onOpenReader: () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier then Modifier.size(50.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xAD212121))
|
||||||
|
.border(0.1.dp, Color(0xFFEDEDED))
|
||||||
|
.clickable(onClick = onOpenReader),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_start_reading_24dp),
|
||||||
|
contentDescription = stringResource(R.string.action_start_reading),
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(24.dp).padding(3.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
import androidx.compose.ui.composed
|
||||||
@ -17,6 +20,15 @@ import androidx.compose.ui.unit.Constraints
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed {
|
||||||
|
if (isSelected) {
|
||||||
|
val alpha = if (isSystemInDarkTheme()) 0.08f else 0.22f
|
||||||
|
background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(.78f)
|
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(.78f)
|
||||||
|
|
||||||
fun Modifier.clickableNoIndication(
|
fun Modifier.clickableNoIndication(
|
||||||
|
@ -3,14 +3,34 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
import eu.kanade.domain.category.model.Category
|
import eu.kanade.domain.category.model.Category
|
||||||
|
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||||
|
import eu.kanade.presentation.library.components.LibraryComfortableGrid
|
||||||
|
import eu.kanade.presentation.library.components.LibraryCompactGrid
|
||||||
|
import eu.kanade.presentation.library.components.LibraryCoverOnlyGrid
|
||||||
|
import eu.kanade.presentation.library.components.LibraryList
|
||||||
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
|
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -21,13 +41,18 @@ import uy.kohesive.injekt.api.get
|
|||||||
*/
|
*/
|
||||||
class LibraryAdapter(
|
class LibraryAdapter(
|
||||||
private val controller: LibraryController,
|
private val controller: LibraryController,
|
||||||
|
private val presenter: LibraryPresenter,
|
||||||
|
private val onClickManga: (LibraryManga) -> Unit,
|
||||||
|
// SY -->
|
||||||
|
private val onOpenReader: (LibraryManga) -> Unit,
|
||||||
|
// SY <--
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
) : RecyclerViewPagerAdapter() {
|
) : RecyclerViewPagerAdapter() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The categories to bind in the adapter.
|
* The categories to bind in the adapter.
|
||||||
*/
|
*/
|
||||||
var categories: List<Category> = emptyList()
|
var categories: List<Category> = mutableStateListOf()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,19 +63,6 @@ class LibraryAdapter(
|
|||||||
|
|
||||||
private var boundViews = arrayListOf<View>()
|
private var boundViews = arrayListOf<View>()
|
||||||
|
|
||||||
private val isPerCategory by lazy { preferences.categorizedDisplaySettings().get() }
|
|
||||||
private var currentDisplayMode = preferences.libraryDisplayMode().get()
|
|
||||||
|
|
||||||
init {
|
|
||||||
preferences.libraryDisplayMode()
|
|
||||||
.asFlow()
|
|
||||||
.drop(1)
|
|
||||||
.onEach {
|
|
||||||
currentDisplayMode = it
|
|
||||||
}
|
|
||||||
.launchIn(controller.viewScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pair of category and size of category
|
* Pair of category and size of category
|
||||||
*/
|
*/
|
||||||
@ -80,10 +92,8 @@ class LibraryAdapter(
|
|||||||
* @return a new view.
|
* @return a new view.
|
||||||
*/
|
*/
|
||||||
override fun inflateView(container: ViewGroup, viewType: Int): View {
|
override fun inflateView(container: ViewGroup, viewType: Int): View {
|
||||||
val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
|
val binding = ComposeControllerBinding.inflate(LayoutInflater.from(container.context), container, false)
|
||||||
val view: LibraryCategoryView = binding.root
|
return binding.root
|
||||||
view.onCreate(controller, binding, viewType)
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +103,98 @@ class LibraryAdapter(
|
|||||||
* @param position the position in the adapter.
|
* @param position the position in the adapter.
|
||||||
*/
|
*/
|
||||||
override fun bindView(view: View, position: Int) {
|
override fun bindView(view: View, position: Int) {
|
||||||
(view as LibraryCategoryView).onBind(categories[position])
|
(view as ComposeView).apply {
|
||||||
|
consumeWindowInsets = false
|
||||||
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
|
setContent {
|
||||||
|
TachiyomiTheme {
|
||||||
|
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
||||||
|
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||||
|
|
||||||
|
val category = presenter.categories[position]
|
||||||
|
val displayMode = presenter.getDisplayMode(index = position)
|
||||||
|
val mangaList by presenter.getMangaForCategory(categoryId = category.id)
|
||||||
|
|
||||||
|
val onClickManga = { manga: LibraryManga ->
|
||||||
|
if (presenter.hasSelection().not()) {
|
||||||
|
onClickManga(manga)
|
||||||
|
} else {
|
||||||
|
presenter.toggleSelection(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onLongClickManga = { manga: LibraryManga ->
|
||||||
|
presenter.toggleSelection(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeRefresh(
|
||||||
|
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||||
|
state = rememberSwipeRefreshState(isRefreshing = false),
|
||||||
|
onRefresh = {
|
||||||
|
if (LibraryUpdateService.start(context, category)) {
|
||||||
|
context.toast(R.string.updating_category)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
indicator = { s, trigger ->
|
||||||
|
SwipeRefreshIndicator(
|
||||||
|
state = s,
|
||||||
|
refreshTriggerDistance = trigger,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
when (displayMode) {
|
||||||
|
DisplayModeSetting.LIST -> {
|
||||||
|
LibraryList(
|
||||||
|
items = mangaList,
|
||||||
|
columns = presenter.columns,
|
||||||
|
selection = presenter.selection,
|
||||||
|
onClick = onClickManga,
|
||||||
|
onLongClick = {
|
||||||
|
presenter.toggleSelection(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisplayModeSetting.COMPACT_GRID -> {
|
||||||
|
LibraryCompactGrid(
|
||||||
|
items = mangaList,
|
||||||
|
columns = presenter.columns,
|
||||||
|
selection = presenter.selection,
|
||||||
|
onClick = onClickManga,
|
||||||
|
onLongClick = onLongClickManga,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader = onOpenReader,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisplayModeSetting.COMFORTABLE_GRID -> {
|
||||||
|
LibraryComfortableGrid(
|
||||||
|
items = mangaList,
|
||||||
|
columns = presenter.columns,
|
||||||
|
selection = presenter.selection,
|
||||||
|
onClick = onClickManga,
|
||||||
|
onLongClick = onLongClickManga,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader = onOpenReader,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisplayModeSetting.COVER_ONLY_GRID -> {
|
||||||
|
LibraryCoverOnlyGrid(
|
||||||
|
items = mangaList,
|
||||||
|
columns = presenter.columns,
|
||||||
|
selection = presenter.selection,
|
||||||
|
onClick = onClickManga,
|
||||||
|
onLongClick = onLongClickManga,
|
||||||
|
// SY -->
|
||||||
|
onOpenReader = onOpenReader,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
boundViews.add(view)
|
boundViews.add(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +205,6 @@ class LibraryAdapter(
|
|||||||
* @param position the position in the adapter.
|
* @param position the position in the adapter.
|
||||||
*/
|
*/
|
||||||
override fun recycleView(view: View, position: Int) {
|
override fun recycleView(view: View, position: Int) {
|
||||||
(view as LibraryCategoryView).onRecycle()
|
|
||||||
boundViews.remove(view)
|
boundViews.remove(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,45 +231,5 @@ class LibraryAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun getViewType(position: Int): Int = -1
|
||||||
* Returns the position of the view.
|
|
||||||
*/
|
|
||||||
override fun getItemPosition(obj: Any): Int {
|
|
||||||
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
|
||||||
val index = categories.indexOfFirst { it.id == view.category.id }
|
|
||||||
return if (index == -1) POSITION_NONE else index
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the view of this adapter is being destroyed.
|
|
||||||
*/
|
|
||||||
fun onDestroy() {
|
|
||||||
for (view in boundViews) {
|
|
||||||
if (view is LibraryCategoryView) {
|
|
||||||
view.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewType(position: Int): Int {
|
|
||||||
val category = categories.getOrNull(position)
|
|
||||||
return if (isPerCategory && category?.id != 0L) {
|
|
||||||
if (DisplayModeSetting.fromFlag(category?.displayMode) == DisplayModeSetting.LIST) {
|
|
||||||
LIST_DISPLAY_MODE
|
|
||||||
} else {
|
|
||||||
GRID_DISPLAY_MODE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentDisplayMode == DisplayModeSetting.LIST) {
|
|
||||||
LIST_DISPLAY_MODE
|
|
||||||
} else {
|
|
||||||
GRID_DISPLAY_MODE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LIST_DISPLAY_MODE = 1
|
|
||||||
const val GRID_DISPLAY_MODE = 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,241 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
|
|
||||||
import eu.kanade.domain.manga.interactor.GetSearchTags
|
|
||||||
import eu.kanade.domain.manga.interactor.GetSearchTitles
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
import eu.kanade.domain.track.interactor.GetTracks
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
|
||||||
import exh.log.xLogW
|
|
||||||
import exh.metadata.sql.models.SearchTag
|
|
||||||
import exh.metadata.sql.models.SearchTitle
|
|
||||||
import exh.search.Namespace
|
|
||||||
import exh.search.QueryComponent
|
|
||||||
import exh.search.SearchEngine
|
|
||||||
import exh.search.Text
|
|
||||||
import exh.source.isMetadataSource
|
|
||||||
import exh.util.cancellable
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter storing a list of manga in a certain category.
|
|
||||||
*
|
|
||||||
* @param view the fragment containing this adapter.
|
|
||||||
*/
|
|
||||||
class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryController) :
|
|
||||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
|
||||||
// EXH -->
|
|
||||||
private val searchEngine = SearchEngine()
|
|
||||||
private var lastFilterJob: Job? = null
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
private val hasLoggedServices by lazy {
|
|
||||||
trackManager.hasLoggedServices()
|
|
||||||
}
|
|
||||||
private val services = trackManager.services.associate { service ->
|
|
||||||
service.id to controller.activity!!.getString(service.nameRes())
|
|
||||||
}
|
|
||||||
private val getIdsOfFavoriteMangaWithMetadata: GetIdsOfFavoriteMangaWithMetadata by injectLazy()
|
|
||||||
private val getTracks: GetTracks by injectLazy()
|
|
||||||
private val getSearchTags: GetSearchTags by injectLazy()
|
|
||||||
private val getSearchTitles: GetSearchTitles by injectLazy()
|
|
||||||
|
|
||||||
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
|
|
||||||
var searchText
|
|
||||||
get() = getFilter(String::class.java).orEmpty()
|
|
||||||
set(value) {
|
|
||||||
setFilter(value)
|
|
||||||
}
|
|
||||||
// EXH <--
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of manga in this category.
|
|
||||||
*/
|
|
||||||
private var mangas: List<LibraryItem> = emptyList()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a list of manga in the adapter.
|
|
||||||
*
|
|
||||||
* @param list the list to set.
|
|
||||||
*/
|
|
||||||
suspend fun setItems(scope: CoroutineScope, list: List<LibraryItem>) {
|
|
||||||
// A copy of manga always unfiltered.
|
|
||||||
mangas = list.toList()
|
|
||||||
|
|
||||||
performFilter(scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the position in the adapter for the given manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to find.
|
|
||||||
*/
|
|
||||||
fun indexOf(manga: Manga): Int {
|
|
||||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canDrag() = (mode != Mode.MULTI || (mode == Mode.MULTI && selectedItemCount == 1)) &&
|
|
||||||
searchText.isBlank() &&
|
|
||||||
preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT &&
|
|
||||||
!preferences.downloadedOnly().get() &&
|
|
||||||
preferences.filterDownloaded().get() == Filter.TriState.STATE_IGNORE &&
|
|
||||||
preferences.filterCompleted().get() == Filter.TriState.STATE_IGNORE &&
|
|
||||||
preferences.filterStarted().get() == Filter.TriState.STATE_IGNORE &&
|
|
||||||
preferences.filterUnread().get() == Filter.TriState.STATE_IGNORE &&
|
|
||||||
services.all { preferences.filterTracking(it.key).get() == Filter.TriState.STATE_IGNORE } &&
|
|
||||||
preferences.filterLewd().get() == Filter.TriState.STATE_IGNORE
|
|
||||||
|
|
||||||
// EXH -->
|
|
||||||
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
|
|
||||||
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
|
|
||||||
// we want to perform a no-op filter)
|
|
||||||
suspend fun performFilter(scope: CoroutineScope) {
|
|
||||||
isLongPressDragEnabled = canDrag()
|
|
||||||
lastFilterJob?.cancel()
|
|
||||||
if (mangas.isNotEmpty() && searchText.isNotBlank()) {
|
|
||||||
val savedSearchText = searchText
|
|
||||||
|
|
||||||
val job = scope.launch(Dispatchers.IO) {
|
|
||||||
val newManga = try {
|
|
||||||
// Prepare filter object
|
|
||||||
val parsedQuery = searchEngine.parseQuery(savedSearchText)
|
|
||||||
val mangaWithMetaIds = getIdsOfFavoriteMangaWithMetadata.await()
|
|
||||||
|
|
||||||
ensureActive() // Fail early when cancelled
|
|
||||||
|
|
||||||
// Flow the mangas to allow cancellation of this filter operation
|
|
||||||
mangas.asFlow().cancellable().filter { item ->
|
|
||||||
if (isMetadataSource(item.manga.source)) {
|
|
||||||
val mangaId = item.manga.id ?: -1
|
|
||||||
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
|
||||||
// No meta? Filter using title
|
|
||||||
filterManga(parsedQuery, item.manga)
|
|
||||||
} else {
|
|
||||||
val tags = getSearchTags.await(mangaId)
|
|
||||||
val titles = getSearchTitles.await(mangaId)
|
|
||||||
filterManga(parsedQuery, item.manga, false, tags, titles)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
filterManga(parsedQuery, item.manga)
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Do not catch cancellations
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
|
|
||||||
this@LibraryCategoryAdapter.xLogW("Could not filter mangas!", e)
|
|
||||||
mangas
|
|
||||||
}
|
|
||||||
|
|
||||||
withUIContext {
|
|
||||||
updateDataSet(newManga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastFilterJob = job
|
|
||||||
job.join()
|
|
||||||
} else {
|
|
||||||
updateDataSet(mangas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun filterManga(
|
|
||||||
queries: List<QueryComponent>,
|
|
||||||
manga: LibraryManga,
|
|
||||||
checkGenre: Boolean = true,
|
|
||||||
searchTags: List<SearchTag>? = null,
|
|
||||||
searchTitles: List<SearchTitle>? = null,
|
|
||||||
): Boolean {
|
|
||||||
val mappedQueries = queries.groupBy { it.excluded }
|
|
||||||
val tracks = if (hasLoggedServices) getTracks.await(manga.id!!).toList() else null
|
|
||||||
val source = sourceManager.get(manga.source)
|
|
||||||
val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList()
|
|
||||||
val hasNormalQuery = mappedQueries[false]?.all { queryComponent ->
|
|
||||||
when (queryComponent) {
|
|
||||||
is Text -> {
|
|
||||||
val query = queryComponent.asQuery()
|
|
||||||
manga.title.contains(query, true) ||
|
|
||||||
(manga.author?.contains(query, true) == true) ||
|
|
||||||
(manga.artist?.contains(query, true) == true) ||
|
|
||||||
(manga.description?.contains(query, true) == true) ||
|
|
||||||
(source?.name?.contains(query, true) == true) ||
|
|
||||||
(hasLoggedServices && tracks != null && filterTracks(query, tracks)) ||
|
|
||||||
(genre.any { it.contains(query, true) }) ||
|
|
||||||
(searchTags.orEmpty().any { it.name.contains(query, true) }) ||
|
|
||||||
(searchTitles.orEmpty().any { it.title.contains(query, true) })
|
|
||||||
}
|
|
||||||
is Namespace -> {
|
|
||||||
searchTags != null && searchTags.any {
|
|
||||||
val tag = queryComponent.tag
|
|
||||||
(it.namespace != null && it.namespace.contains(queryComponent.namespace, true) && tag != null && it.name.contains(tag.asQuery(), true)) ||
|
|
||||||
(tag == null && it.namespace != null && it.namespace.contains(queryComponent.namespace, true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val doesNotHaveExcludedQuery = mappedQueries[true]?.all { queryComponent ->
|
|
||||||
when (queryComponent) {
|
|
||||||
is Text -> {
|
|
||||||
val query = queryComponent.asQuery()
|
|
||||||
query.isBlank() || (
|
|
||||||
(!manga.title.contains(query, true)) &&
|
|
||||||
(!manga.author.orEmpty().contains(query, true)) &&
|
|
||||||
(!manga.artist.orEmpty().contains(query, true)) &&
|
|
||||||
(!manga.description.orEmpty().contains(query, true)) &&
|
|
||||||
(!source?.name.orEmpty().contains(query, true)) &&
|
|
||||||
(!hasLoggedServices || hasLoggedServices && tracks == null || tracks != null && !filterTracks(query, tracks)) &&
|
|
||||||
(genre.none { it.contains(query, true) }) &&
|
|
||||||
(searchTags.orEmpty().none { it.name.contains(query, true) }) &&
|
|
||||||
(searchTitles.orEmpty().none { it.title.contains(query, true) })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is Namespace -> {
|
|
||||||
val searchedTag = queryComponent.tag?.asQuery()
|
|
||||||
searchTags == null || searchTags.all { mangaTag ->
|
|
||||||
if (searchedTag == null || searchedTag.isBlank()) {
|
|
||||||
mangaTag.namespace == null || !mangaTag.namespace.contains(queryComponent.namespace, true)
|
|
||||||
} else if (mangaTag.namespace == null) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
!(mangaTag.name.contains(searchedTag, true) && mangaTag.namespace.contains(queryComponent.namespace, true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (hasNormalQuery != null && doesNotHaveExcludedQuery != null && hasNormalQuery && doesNotHaveExcludedQuery) ||
|
|
||||||
(hasNormalQuery != null && doesNotHaveExcludedQuery == null && hasNormalQuery) ||
|
|
||||||
(hasNormalQuery == null && doesNotHaveExcludedQuery != null && doesNotHaveExcludedQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterTracks(constraint: String, tracks: List<eu.kanade.domain.track.model.Track>): Boolean {
|
|
||||||
return tracks.any {
|
|
||||||
val trackService = trackManager.getService(it.syncId)
|
|
||||||
if (trackService != null) {
|
|
||||||
val status = trackService.getStatus(it.status.toInt())
|
|
||||||
val name = services[it.syncId]
|
|
||||||
return@any status.contains(constraint, true) || name?.contains(constraint, true) == true
|
|
||||||
}
|
|
||||||
return@any false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// EXH <--
|
|
||||||
}
|
|
@ -1,491 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
|
||||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
|
||||||
import eu.kanade.domain.category.model.Category
|
|
||||||
import eu.kanade.domain.category.model.CategoryUpdate
|
|
||||||
import eu.kanade.domain.category.model.toDbCategory
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.inflate
|
|
||||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
|
||||||
import exh.ui.LoadingHandle
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
|
||||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.subscriptions.CompositeSubscription
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.ArrayDeque
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment containing the library manga for a certain category.
|
|
||||||
*/
|
|
||||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
|
||||||
FrameLayout(context, attrs),
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
|
||||||
// SY -->
|
|
||||||
FlexibleAdapter.OnItemMoveListener {
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
private val scope = MainScope()
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The fragment containing this view.
|
|
||||||
*/
|
|
||||||
private lateinit var controller: LibraryController
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Category for this view.
|
|
||||||
*/
|
|
||||||
lateinit var category: Category
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recycler view of the list of manga.
|
|
||||||
*/
|
|
||||||
private lateinit var recycler: AutofitRecyclerView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter to hold the manga in this category.
|
|
||||||
*/
|
|
||||||
private lateinit var adapter: LibraryCategoryAdapter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscriptions while the view is bound.
|
|
||||||
*/
|
|
||||||
private var subscriptions = CompositeSubscription()
|
|
||||||
|
|
||||||
private var lastClickPositionStack = ArrayDeque(listOf(-1))
|
|
||||||
|
|
||||||
// EXH -->
|
|
||||||
private var initialLoadHandle: LoadingHandle? = null
|
|
||||||
// EXH <--
|
|
||||||
|
|
||||||
fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding, viewType: Int) {
|
|
||||||
this.controller = controller
|
|
||||||
|
|
||||||
recycler = if (viewType == LibraryAdapter.LIST_DISPLAY_MODE) {
|
|
||||||
(binding.swipeRefresh.inflate(R.layout.library_list_recycler) as AutofitRecyclerView).apply {
|
|
||||||
spanCount = 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(binding.swipeRefresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
|
||||||
spanCount = controller.mangaPerRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recycler.applyInsetter {
|
|
||||||
type(navigationBars = true) {
|
|
||||||
padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = LibraryCategoryAdapter(this, controller)
|
|
||||||
|
|
||||||
recycler.setHasFixedSize(true)
|
|
||||||
recycler.adapter = adapter
|
|
||||||
binding.swipeRefresh.addView(recycler)
|
|
||||||
adapter.fastScroller = binding.fastScroller
|
|
||||||
|
|
||||||
recycler.scrollStateChanges()
|
|
||||||
.onEach {
|
|
||||||
// Disable swipe refresh when view is not at the top
|
|
||||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
|
||||||
.findFirstCompletelyVisibleItemPosition()
|
|
||||||
binding.swipeRefresh.isEnabled = firstPos <= 0
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
recycler.onAnimationsFinished {
|
|
||||||
(controller.activity as? MainActivity)?.ready = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double the distance required to trigger sync
|
|
||||||
binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
|
||||||
binding.swipeRefresh.refreshes()
|
|
||||||
.onEach {
|
|
||||||
// SY -->
|
|
||||||
if (LibraryUpdateService.start(context, if (controller.presenter.groupType == LibraryGroup.BY_DEFAULT) category else null, group = controller.presenter.groupType, groupExtra = getGroupExtra())) {
|
|
||||||
context.toast(
|
|
||||||
when {
|
|
||||||
controller.presenter.groupType == LibraryGroup.BY_DEFAULT ||
|
|
||||||
(preferences.groupLibraryUpdateType().get() == PreferenceValues.GroupLibraryMode.ALL) -> R.string.updating_category
|
|
||||||
(
|
|
||||||
controller.presenter.groupType == LibraryGroup.UNGROUPED &&
|
|
||||||
preferences.groupLibraryUpdateType().get() == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED
|
|
||||||
) ||
|
|
||||||
preferences.groupLibraryUpdateType().get() == PreferenceValues.GroupLibraryMode.GLOBAL -> R.string.updating_library
|
|
||||||
else -> R.string.updating_category
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
|
||||||
binding.swipeRefresh.isRefreshing = false
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(category: Category) {
|
|
||||||
this.category = category
|
|
||||||
|
|
||||||
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
|
||||||
SelectableAdapter.Mode.MULTI
|
|
||||||
} else {
|
|
||||||
SelectableAdapter.Mode.SINGLE
|
|
||||||
}
|
|
||||||
// SY -->
|
|
||||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// EXH -->
|
|
||||||
initialLoadHandle = controller.loaderManager.openProgressBar()
|
|
||||||
// EXH <--
|
|
||||||
|
|
||||||
subscriptions += controller.searchRelay
|
|
||||||
.doOnNext { adapter.searchText = it }
|
|
||||||
.skip(1)
|
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
// EXH -->
|
|
||||||
scope.launch {
|
|
||||||
val handle = controller.loaderManager.openProgressBar()
|
|
||||||
try {
|
|
||||||
// EXH <--
|
|
||||||
adapter.performFilter(this)
|
|
||||||
// EXH -->
|
|
||||||
} finally {
|
|
||||||
controller.loaderManager.closeProgressBar(handle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// EXH <--
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions += controller.libraryMangaRelay
|
|
||||||
.subscribe {
|
|
||||||
// EXH -->
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
// EXH <--
|
|
||||||
onNextLibraryManga(this, it)
|
|
||||||
// EXH -->
|
|
||||||
} finally {
|
|
||||||
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// EXH <--
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions += controller.selectionRelay
|
|
||||||
.subscribe { onSelectionChanged(it) }
|
|
||||||
|
|
||||||
subscriptions += controller.selectAllRelay
|
|
||||||
.filter { it == category.id }
|
|
||||||
.subscribe {
|
|
||||||
adapter.currentItems.forEach { item ->
|
|
||||||
controller.setSelection(item.manga.toDomainManga()!!, true)
|
|
||||||
}
|
|
||||||
controller.invalidateActionMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions += controller.selectInverseRelay
|
|
||||||
.filter { it == category.id }
|
|
||||||
.subscribe {
|
|
||||||
adapter.currentItems.forEach { item ->
|
|
||||||
controller.toggleSelection(item.manga.toDomainManga()!!)
|
|
||||||
}
|
|
||||||
controller.invalidateActionMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRecycle() {
|
|
||||||
// SY -->
|
|
||||||
runBlocking { adapter.setItems(this, emptyList()) }
|
|
||||||
// SY <--
|
|
||||||
adapter.clearSelection()
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDestroy() {
|
|
||||||
unsubscribe()
|
|
||||||
scope.cancel()
|
|
||||||
// SY -->
|
|
||||||
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unsubscribe() {
|
|
||||||
subscriptions.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
|
|
||||||
* adapter.
|
|
||||||
*
|
|
||||||
* @param event the event received.
|
|
||||||
*/
|
|
||||||
private suspend fun onNextLibraryManga(cScope: CoroutineScope, event: LibraryMangaEvent) {
|
|
||||||
// Get the manga list for this category.
|
|
||||||
// SY -->
|
|
||||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
|
||||||
var mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
|
||||||
var mangaOrder = category.mangaOrder
|
|
||||||
if (preferences.categorizedDisplaySettings().get() && category.id != 0L) {
|
|
||||||
if (SortModeSetting.fromFlag(category.sortMode) == SortModeSetting.DRAG_AND_DROP) {
|
|
||||||
mangaForCategory = mangaForCategory.sortedBy {
|
|
||||||
mangaOrder.indexOf(it.manga.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (preferences.librarySortingMode().get() == SortModeSetting.DRAG_AND_DROP) {
|
|
||||||
if (category.id == 0L) {
|
|
||||||
mangaOrder = preferences.defaultMangaOrder().get()
|
|
||||||
.split("/")
|
|
||||||
.mapNotNull { it.toLongOrNull() }
|
|
||||||
}
|
|
||||||
mangaForCategory = mangaForCategory.sortedBy {
|
|
||||||
mangaOrder.indexOf(it.manga.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
// Update the category with its manga.
|
|
||||||
// EXH -->
|
|
||||||
adapter.setItems(cScope, mangaForCategory)
|
|
||||||
// EXH <--
|
|
||||||
|
|
||||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
|
||||||
controller.selectedMangas.forEach { manga ->
|
|
||||||
val position = adapter.indexOf(manga)
|
|
||||||
if (position != -1 && !adapter.isSelected(position)) {
|
|
||||||
adapter.toggleSelection(position)
|
|
||||||
(recycler.findViewHolderForItemId(manga.id) as? LibraryHolder<*>)?.toggleActivation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
|
|
||||||
* depending on the type of event received.
|
|
||||||
*
|
|
||||||
* @param event the selection event received.
|
|
||||||
*/
|
|
||||||
private fun onSelectionChanged(event: LibrarySelectionEvent) {
|
|
||||||
when (event) {
|
|
||||||
is LibrarySelectionEvent.Selected -> {
|
|
||||||
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
|
||||||
adapter.mode = SelectableAdapter.Mode.MULTI
|
|
||||||
// SY -->
|
|
||||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
findAndToggleSelection(event.manga)
|
|
||||||
}
|
|
||||||
is LibrarySelectionEvent.Unselected -> {
|
|
||||||
findAndToggleSelection(event.manga)
|
|
||||||
|
|
||||||
with(adapter.indexOf(event.manga)) {
|
|
||||||
if (this != -1) lastClickPositionStack.remove(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.selectedMangas.isEmpty()) {
|
|
||||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
|
||||||
// SY -->
|
|
||||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is LibrarySelectionEvent.Cleared -> {
|
|
||||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
|
||||||
adapter.clearSelection()
|
|
||||||
|
|
||||||
lastClickPositionStack.clear()
|
|
||||||
lastClickPositionStack.push(-1)
|
|
||||||
// SY -->
|
|
||||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the selection for the given manga and updates the view if needed.
|
|
||||||
*
|
|
||||||
* @param manga the manga to toggle.
|
|
||||||
*/
|
|
||||||
private fun findAndToggleSelection(manga: Manga) {
|
|
||||||
val position = adapter.indexOf(manga)
|
|
||||||
if (position != -1) {
|
|
||||||
adapter.toggleSelection(position)
|
|
||||||
(recycler.findViewHolderForItemId(manga.id) as? LibraryHolder<*>)?.toggleActivation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a manga is clicked.
|
|
||||||
*
|
|
||||||
* @param position the position of the element clicked.
|
|
||||||
* @return true if the item should be selected, false otherwise.
|
|
||||||
*/
|
|
||||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
|
||||||
// If the action mode is created and the position is valid, toggle the selection.
|
|
||||||
val item = adapter.getItem(position) ?: return false
|
|
||||||
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
|
||||||
if (adapter.isSelected(position)) {
|
|
||||||
lastClickPositionStack.remove(position)
|
|
||||||
} else {
|
|
||||||
lastClickPositionStack.push(position)
|
|
||||||
}
|
|
||||||
toggleSelection(position)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
openManga(item.manga.toDomainManga()!!)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a manga is long clicked.
|
|
||||||
*
|
|
||||||
* @param position the position of the element clicked.
|
|
||||||
*/
|
|
||||||
override fun onItemLongClick(position: Int) {
|
|
||||||
controller.createActionModeIfNeeded()
|
|
||||||
val lastClickPosition = lastClickPositionStack.peek()!!
|
|
||||||
// SY -->
|
|
||||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
|
||||||
// SY <--
|
|
||||||
when {
|
|
||||||
lastClickPosition == -1 -> setSelection(position)
|
|
||||||
lastClickPosition > position ->
|
|
||||||
for (i in position until lastClickPosition)
|
|
||||||
setSelection(i)
|
|
||||||
lastClickPosition < position ->
|
|
||||||
for (i in lastClickPosition + 1..position)
|
|
||||||
setSelection(i)
|
|
||||||
else -> setSelection(position)
|
|
||||||
}
|
|
||||||
if (lastClickPosition != position) {
|
|
||||||
lastClickPositionStack.remove(position)
|
|
||||||
lastClickPositionStack.push(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
private fun getGroupExtra() = when (controller.presenter.groupType) {
|
|
||||||
LibraryGroup.BY_DEFAULT -> null
|
|
||||||
LibraryGroup.BY_SOURCE, LibraryGroup.BY_STATUS, LibraryGroup.BY_TRACK_STATUS -> category.id.toString()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
|
||||||
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
|
||||||
val position = viewHolder?.bindingAdapterPosition ?: return
|
|
||||||
if (actionState == 2) {
|
|
||||||
onItemLongClick(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val updateCategory: UpdateCategory by injectLazy()
|
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
|
||||||
if (fromPosition == toPosition) return
|
|
||||||
controller.invalidateActionMode()
|
|
||||||
val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
|
|
||||||
if (category.id == 0L) {
|
|
||||||
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
|
|
||||||
} else {
|
|
||||||
scope.launch {
|
|
||||||
updateCategory.await(CategoryUpdate(category.id.toLong(), mangaOrder = mangaIds))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (preferences.categorizedDisplaySettings().get() && category.id != 0L) {
|
|
||||||
if (SortModeSetting.fromFlag(category.sortMode) != SortModeSetting.DRAG_AND_DROP) {
|
|
||||||
val dbCategory = category.toDbCategory()
|
|
||||||
dbCategory.sortMode = SortModeSetting.DRAG_AND_DROP.flag.toInt()
|
|
||||||
dbCategory.sortDirection = SortDirectionSetting.ASCENDING.flag.toInt()
|
|
||||||
scope.launch {
|
|
||||||
updateCategory.await(
|
|
||||||
CategoryUpdate(
|
|
||||||
id = category.id,
|
|
||||||
flags = dbCategory.flags.toLong(),
|
|
||||||
mangaOrder = mangaIds,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (preferences.librarySortingMode().get() != SortModeSetting.DRAG_AND_DROP) {
|
|
||||||
preferences.librarySortingAscending().set(SortDirectionSetting.ASCENDING)
|
|
||||||
preferences.librarySortingMode().set(SortModeSetting.DRAG_AND_DROP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to open.
|
|
||||||
*/
|
|
||||||
private fun openManga(manga: Manga) {
|
|
||||||
controller.openManga(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the presenter to toggle the selection for the given position.
|
|
||||||
*
|
|
||||||
* @param position the position to toggle.
|
|
||||||
*/
|
|
||||||
private fun toggleSelection(position: Int) {
|
|
||||||
val item = adapter.getItem(position) ?: return
|
|
||||||
|
|
||||||
controller.setSelection(item.manga.toDomainManga()!!, !adapter.isSelected(position))
|
|
||||||
controller.invalidateActionMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the presenter to set the selection for the given position.
|
|
||||||
*
|
|
||||||
* @param position the position to toggle.
|
|
||||||
*/
|
|
||||||
private fun setSelection(position: Int) {
|
|
||||||
val item = adapter.getItem(position) ?: return
|
|
||||||
|
|
||||||
controller.setSelection(item.manga.toDomainManga()!!, true)
|
|
||||||
controller.invalidateActionMode()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.dispose
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
|
||||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
|
||||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
|
||||||
* All the elements from the layout file "item_source_grid" are available in this class.
|
|
||||||
*
|
|
||||||
* @param binding the inflated view for this holder.
|
|
||||||
* @param adapter the adapter handling this holder.
|
|
||||||
* @param listener a listener to react to single tap and long tap events.
|
|
||||||
* @constructor creates a new library holder.
|
|
||||||
*/
|
|
||||||
class LibraryComfortableGridHolder(
|
|
||||||
override val binding: SourceComfortableGridItemBinding,
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
) : LibraryHolder<SourceComfortableGridItemBinding>(binding.root, adapter) {
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
var manga: Manga? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.playLayout.clicks()
|
|
||||||
.onEach {
|
|
||||||
playButtonClicked()
|
|
||||||
}
|
|
||||||
.launchIn((adapter as LibraryCategoryAdapter).controller.viewScope)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
|
||||||
* holder with the given manga.
|
|
||||||
*
|
|
||||||
* @param item the manga item to bind.
|
|
||||||
*/
|
|
||||||
override fun onSetValues(item: LibraryItem) {
|
|
||||||
// SY -->
|
|
||||||
manga = item.manga
|
|
||||||
// SY <--
|
|
||||||
// Update the title of the manga.
|
|
||||||
binding.title.text = item.manga.title
|
|
||||||
|
|
||||||
// For rounded corners
|
|
||||||
binding.badges.leftBadges.clipToOutline = true
|
|
||||||
binding.badges.rightBadges.clipToOutline = true
|
|
||||||
|
|
||||||
// Update the unread count and its visibility.
|
|
||||||
with(binding.badges.unreadText) {
|
|
||||||
isVisible = item.unreadCount > 0
|
|
||||||
text = item.unreadCount.toString()
|
|
||||||
}
|
|
||||||
// Update the download count and its visibility.
|
|
||||||
with(binding.badges.downloadText) {
|
|
||||||
isVisible = item.downloadCount > 0
|
|
||||||
text = item.downloadCount.toString()
|
|
||||||
}
|
|
||||||
// Update the source language and its visibility
|
|
||||||
with(binding.badges.languageText) {
|
|
||||||
isVisible = item.sourceLanguage.isNotEmpty()
|
|
||||||
text = item.sourceLanguage
|
|
||||||
}
|
|
||||||
// set local visibility if its local manga
|
|
||||||
binding.badges.localText.isVisible = item.isLocal
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
binding.playLayout.isVisible = (item.manga.unreadCount > 0 && item.startReadingButton)
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// Update the cover.
|
|
||||||
binding.thumbnail.dispose()
|
|
||||||
binding.thumbnail.loadAutoPause(item.manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
private fun playButtonClicked() {
|
|
||||||
if (adapter !is LibraryCategoryAdapter) return
|
|
||||||
adapter.controller.startReading(manga?.toDomainManga() ?: return, adapter)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import coil.dispose
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
|
||||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
|
||||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
|
||||||
* All the elements from the layout file "source_compact_grid_item" are available in this class.
|
|
||||||
*
|
|
||||||
* @param binding the inflated view for this holder.
|
|
||||||
* @param adapter the adapter handling this holder.
|
|
||||||
* @param coverOnly true if title should be hidden a.k.a cover only mode.
|
|
||||||
* @constructor creates a new library holder.
|
|
||||||
*/
|
|
||||||
class LibraryCompactGridHolder(
|
|
||||||
override val binding: SourceCompactGridItemBinding,
|
|
||||||
adapter: FlexibleAdapter<*>,
|
|
||||||
private val coverOnly: Boolean,
|
|
||||||
) : LibraryHolder<SourceCompactGridItemBinding>(binding.root, adapter) {
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
var manga: Manga? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.playLayout.clicks()
|
|
||||||
.onEach {
|
|
||||||
playButtonClicked()
|
|
||||||
}
|
|
||||||
.launchIn((adapter as LibraryCategoryAdapter).controller.viewScope)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
|
||||||
* holder with the given manga.
|
|
||||||
*
|
|
||||||
* @param item the manga item to bind.
|
|
||||||
*/
|
|
||||||
override fun onSetValues(item: LibraryItem) {
|
|
||||||
// SY -->
|
|
||||||
manga = item.manga
|
|
||||||
// SY <--
|
|
||||||
// Update the title of the manga.
|
|
||||||
binding.title.text = item.manga.title
|
|
||||||
|
|
||||||
// For rounded corners
|
|
||||||
binding.badges.leftBadges.clipToOutline = true
|
|
||||||
binding.badges.rightBadges.clipToOutline = true
|
|
||||||
|
|
||||||
// Update the unread count and its visibility.
|
|
||||||
with(binding.badges.unreadText) {
|
|
||||||
isVisible = item.unreadCount > 0
|
|
||||||
text = item.unreadCount.toString()
|
|
||||||
}
|
|
||||||
// Update the download count and its visibility.
|
|
||||||
with(binding.badges.downloadText) {
|
|
||||||
isVisible = item.downloadCount > 0
|
|
||||||
text = item.downloadCount.toString()
|
|
||||||
}
|
|
||||||
// Update the source language and its visibility
|
|
||||||
with(binding.badges.languageText) {
|
|
||||||
isVisible = item.sourceLanguage.isNotEmpty()
|
|
||||||
text = item.sourceLanguage
|
|
||||||
}
|
|
||||||
// set local visibility if its local manga
|
|
||||||
binding.badges.localText.isVisible = item.isLocal
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
binding.playLayout.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
when {
|
|
||||||
coverOnly -> {
|
|
||||||
topToBottom = -1
|
|
||||||
topToTop = -1
|
|
||||||
bottomToBottom = binding.thumbnail.id
|
|
||||||
}
|
|
||||||
item.sourceLanguage.isNotEmpty() -> {
|
|
||||||
topToBottom = binding.badges.root.id
|
|
||||||
topToTop = -1
|
|
||||||
bottomToBottom = -1
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
topToBottom = -1
|
|
||||||
topToTop = binding.thumbnail.id
|
|
||||||
bottomToBottom = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.playLayout.isVisible = (item.manga.unreadCount > 0 && item.startReadingButton)
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// Update the cover.
|
|
||||||
binding.thumbnail.dispose()
|
|
||||||
if (coverOnly) {
|
|
||||||
// Cover only mode: Hides title text unless thumbnail is unavailable
|
|
||||||
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
|
|
||||||
binding.thumbnail.loadAutoPause(item.manga)
|
|
||||||
binding.title.isVisible = false
|
|
||||||
} else {
|
|
||||||
binding.title.text = item.manga.title
|
|
||||||
binding.title.isVisible = true
|
|
||||||
}
|
|
||||||
binding.thumbnail.foreground = null
|
|
||||||
} else {
|
|
||||||
binding.thumbnail.loadAutoPause(item.manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
private fun playButtonClicked() {
|
|
||||||
if (adapter !is LibraryCategoryAdapter) return
|
|
||||||
adapter.controller.startReading(manga?.toDomainManga() ?: return, adapter)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
|
@ -17,18 +17,16 @@ import com.fredporciuncula.flow.preferences.Preference
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
|
||||||
import eu.kanade.domain.category.model.Category
|
import eu.kanade.domain.category.model.Category
|
||||||
import eu.kanade.domain.category.model.toDbCategory
|
import eu.kanade.domain.category.model.toDbCategory
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.toDbManga
|
import eu.kanade.domain.manga.model.toDbManga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
@ -40,7 +38,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
|||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@ -97,42 +94,11 @@ class LibraryController(
|
|||||||
*/
|
*/
|
||||||
private var actionMode: ActionModeWithToolbar? = null
|
private var actionMode: ActionModeWithToolbar? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently selected mangas.
|
|
||||||
*/
|
|
||||||
val selectedMangas = mutableSetOf<Manga>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay to notify the UI of selection updates.
|
|
||||||
*/
|
|
||||||
val selectionRelay: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay to notify search query changes.
|
|
||||||
*/
|
|
||||||
val searchRelay: BehaviorRelay<String> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay to notify the library's viewpager for updates.
|
* Relay to notify the library's viewpager for updates.
|
||||||
*/
|
*/
|
||||||
val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay to notify the library's viewpager to select all manga
|
|
||||||
*/
|
|
||||||
val selectAllRelay: PublishRelay<Long> = PublishRelay.create()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay to notify the library's viewpager to select the inverse
|
|
||||||
*/
|
|
||||||
val selectInverseRelay: PublishRelay<Long> = PublishRelay.create()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of manga per row in grid mode.
|
|
||||||
*/
|
|
||||||
var mangaPerRow = 0
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter of the view pager.
|
* Adapter of the view pager.
|
||||||
*/
|
*/
|
||||||
@ -210,7 +176,24 @@ class LibraryController(
|
|||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
adapter = LibraryAdapter(this)
|
adapter = LibraryAdapter(
|
||||||
|
controller = this,
|
||||||
|
presenter = presenter,
|
||||||
|
onClickManga = {
|
||||||
|
openManga(it.id!!)
|
||||||
|
},
|
||||||
|
// SY -->
|
||||||
|
onOpenReader = {
|
||||||
|
startReading(it.toDomainManga()!!)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
|
||||||
|
getColumnsPreferenceForCurrentOrientation()
|
||||||
|
.asFlow()
|
||||||
|
.onEach { presenter.columns = it }
|
||||||
|
.launchIn(viewScope)
|
||||||
|
|
||||||
binding.libraryPager.adapter = adapter
|
binding.libraryPager.adapter = adapter
|
||||||
binding.libraryPager.pageSelections()
|
binding.libraryPager.pageSelections()
|
||||||
.drop(1)
|
.drop(1)
|
||||||
@ -221,13 +204,7 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
|
|
||||||
getColumnsPreferenceForCurrentOrientation().asImmediateFlow { mangaPerRow = it }
|
if (adapter!!.categories.isNotEmpty()) {
|
||||||
.drop(1)
|
|
||||||
// Set again the adapter to recalculate the covers height
|
|
||||||
.onEach { reattachAdapter() }
|
|
||||||
.launchIn(viewScope)
|
|
||||||
|
|
||||||
if (selectedMangas.isNotEmpty()) {
|
|
||||||
createActionModeIfNeeded()
|
createActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +238,14 @@ class LibraryController(
|
|||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||||
|
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
|
preferences.portraitColumns()
|
||||||
|
} else {
|
||||||
|
preferences.landscapeColumns()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
super.onChangeStarted(handler, type)
|
super.onChangeStarted(handler, type)
|
||||||
if (type.isEnter) {
|
if (type.isEnter) {
|
||||||
@ -271,7 +256,6 @@ class LibraryController(
|
|||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
adapter?.onDestroy()
|
|
||||||
adapter = null
|
adapter = null
|
||||||
settingsSheet?.sheetScope?.cancel()
|
settingsSheet?.sheetScope?.cancel()
|
||||||
settingsSheet = null
|
settingsSheet = null
|
||||||
@ -355,6 +339,12 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
presenter.loadedManga.clear()
|
||||||
|
mangaMap.forEach {
|
||||||
|
presenter.loadedManga[it.key] = it.value
|
||||||
|
}
|
||||||
|
presenter.loadedMangaFlow.value = presenter.loadedManga
|
||||||
|
|
||||||
// Send the manga map to child fragments after the adapter is updated.
|
// Send the manga map to child fragments after the adapter is updated.
|
||||||
libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
|
libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
|
||||||
|
|
||||||
@ -362,19 +352,6 @@ class LibraryController(
|
|||||||
updateTitle()
|
updateTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a preference for the number of manga per row based on the current orientation.
|
|
||||||
*
|
|
||||||
* @return the preference.
|
|
||||||
*/
|
|
||||||
private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
|
||||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
preferences.portraitColumns()
|
|
||||||
} else {
|
|
||||||
preferences.landscapeColumns()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFilterChanged() {
|
private fun onFilterChanged() {
|
||||||
presenter.requestFilterUpdate()
|
presenter.requestFilterUpdate()
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
@ -462,7 +439,6 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun performSearch() {
|
private fun performSearch() {
|
||||||
searchRelay.call(presenter.query)
|
|
||||||
if (presenter.query.isNotEmpty()) {
|
if (presenter.query.isNotEmpty()) {
|
||||||
binding.btnGlobalSearch.isVisible = true
|
binding.btnGlobalSearch.isVisible = true
|
||||||
binding.btnGlobalSearch.text =
|
binding.btnGlobalSearch.text =
|
||||||
@ -533,7 +509,7 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
val count = selectedMangas.size
|
val count = presenter.selection.size
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
// Destroy action mode if there are no items selected.
|
// Destroy action mode if there are no items selected.
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
@ -544,17 +520,17 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
||||||
if (selectedMangas.isEmpty()) return
|
if (presenter.hasSelection().not()) return
|
||||||
toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible =
|
toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible =
|
||||||
selectedMangas.any { it.source != LocalSource.ID }
|
presenter.selection.any { presenter.loadedManga.values.any { it.any { it.isLocal } } }
|
||||||
// SY -->
|
// SY -->
|
||||||
toolbar.findToolbarItem(R.id.action_clean)?.isVisible = selectedMangas.any {
|
toolbar.findToolbarItem(R.id.action_clean)?.isVisible = presenter.selection.any {
|
||||||
it.isEhBasedManga() ||
|
it.isEhBasedManga() ||
|
||||||
it.source in nHentaiSourceIds ||
|
it.source in nHentaiSourceIds ||
|
||||||
it.source == PERV_EDEN_EN_SOURCE_ID ||
|
it.source == PERV_EDEN_EN_SOURCE_ID ||
|
||||||
it.source == PERV_EDEN_IT_SOURCE_ID
|
it.source == PERV_EDEN_IT_SOURCE_ID
|
||||||
}
|
}
|
||||||
toolbar.findToolbarItem(R.id.action_push_to_mdlist)?.isVisible = trackManager.mdList.isLogged && selectedMangas.any {
|
toolbar.findToolbarItem(R.id.action_push_to_mdlist)?.isVisible = trackManager.mdList.isLogged && presenter.selection.any {
|
||||||
it.source in mangaDexSourceIds
|
it.source in mangaDexSourceIds
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@ -572,7 +548,7 @@ class LibraryController(
|
|||||||
// SY -->
|
// SY -->
|
||||||
R.id.action_migrate -> {
|
R.id.action_migrate -> {
|
||||||
val skipPre = preferences.skipPreMigration().get()
|
val skipPre = preferences.skipPreMigration().get()
|
||||||
val selectedMangaIds = selectedMangas.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id }
|
val selectedMangaIds = presenter.selection.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id }
|
||||||
clearSelection()
|
clearSelection()
|
||||||
if (selectedMangaIds.isNotEmpty()) {
|
if (selectedMangaIds.isNotEmpty()) {
|
||||||
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
|
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
|
||||||
@ -590,50 +566,18 @@ class LibraryController(
|
|||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
// Clear all the manga selections and notify child views.
|
// Clear all the manga selections and notify child views.
|
||||||
selectedMangas.clear()
|
presenter.clearSelection()
|
||||||
selectionRelay.call(LibrarySelectionEvent.Cleared)
|
|
||||||
|
|
||||||
(activity as? MainActivity)?.showBottomNav(true)
|
(activity as? MainActivity)?.showBottomNav(true)
|
||||||
|
|
||||||
actionMode = null
|
actionMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openManga(manga: Manga) {
|
fun openManga(mangaId: Long) {
|
||||||
// Notify the presenter a manga is being opened.
|
// Notify the presenter a manga is being opened.
|
||||||
presenter.onOpenManga()
|
presenter.onOpenManga()
|
||||||
|
|
||||||
router.pushController(MangaController(manga.id))
|
router.pushController(MangaController(mangaId))
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the selection for a given manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose selection has changed.
|
|
||||||
* @param selected whether it's now selected or not.
|
|
||||||
*/
|
|
||||||
fun setSelection(manga: Manga, selected: Boolean) {
|
|
||||||
if (selected) {
|
|
||||||
if (selectedMangas.add(manga)) {
|
|
||||||
selectionRelay.call(LibrarySelectionEvent.Selected(manga))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedMangas.remove(manga)) {
|
|
||||||
selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the current selection state for a given manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose selection to change.
|
|
||||||
*/
|
|
||||||
fun toggleSelection(manga: Manga) {
|
|
||||||
if (selectedMangas.add(manga)) {
|
|
||||||
selectionRelay.call(LibrarySelectionEvent.Selected(manga))
|
|
||||||
} else if (selectedMangas.remove(manga)) {
|
|
||||||
selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -641,8 +585,7 @@ class LibraryController(
|
|||||||
* invalidate the action mode to revert the top toolbar
|
* invalidate the action mode to revert the top toolbar
|
||||||
*/
|
*/
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
selectedMangas.clear()
|
presenter.clearSelection()
|
||||||
selectionRelay.call(LibrarySelectionEvent.Cleared)
|
|
||||||
invalidateActionMode()
|
invalidateActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,15 +595,15 @@ class LibraryController(
|
|||||||
private fun showMangaCategoriesDialog() {
|
private fun showMangaCategoriesDialog() {
|
||||||
viewScope.launchIO {
|
viewScope.launchIO {
|
||||||
// Create a copy of selected manga
|
// Create a copy of selected manga
|
||||||
val mangas = selectedMangas.toList()
|
val mangas = presenter.selection.toList()
|
||||||
|
|
||||||
// Hide the default category because it has a different behavior than the ones from db.
|
// Hide the default category because it has a different behavior than the ones from db.
|
||||||
val categories = presenter.categories.filter { it.id != 0L }
|
val categories = presenter.categories.filter { it.id != 0L }
|
||||||
|
|
||||||
// Get indexes of the common categories to preselect.
|
// Get indexes of the common categories to preselect.
|
||||||
val common = presenter.getCommonCategories(mangas)
|
val common = presenter.getCommonCategories(mangas.mapNotNull { it.toDomainManga() })
|
||||||
// Get indexes of the mix categories to preselect.
|
// Get indexes of the mix categories to preselect.
|
||||||
val mix = presenter.getMixCategories(mangas)
|
val mix = presenter.getMixCategories(mangas.mapNotNull { it.toDomainManga() })
|
||||||
val preselected = categories.map {
|
val preselected = categories.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
in common -> QuadStateTextView.State.CHECKED.ordinal
|
in common -> QuadStateTextView.State.CHECKED.ordinal
|
||||||
@ -669,31 +612,32 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
launchUI {
|
launchUI {
|
||||||
ChangeMangaCategoriesDialog(this@LibraryController, mangas, categories, preselected)
|
ChangeMangaCategoriesDialog(this@LibraryController, mangas.mapNotNull { it.toDomainManga() }, categories, preselected)
|
||||||
.showDialog(router)
|
.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadUnreadChapters() {
|
private fun downloadUnreadChapters() {
|
||||||
val mangas = selectedMangas.toList()
|
val mangas = presenter.selection.toList()
|
||||||
presenter.downloadUnreadChapters(mangas)
|
presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() })
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun markReadStatus(read: Boolean) {
|
private fun markReadStatus(read: Boolean) {
|
||||||
val mangas = selectedMangas.toList()
|
val mangas = presenter.selection.toList()
|
||||||
presenter.markReadStatus(mangas, read)
|
presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read)
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showDeleteMangaDialog() {
|
private fun showDeleteMangaDialog() {
|
||||||
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
|
val mangas = presenter.selection.toList()
|
||||||
|
DeleteLibraryMangasDialog(this, mangas.mapNotNull { it.toDomainManga() }).showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun cleanTitles() {
|
private fun cleanTitles() {
|
||||||
val mangas = selectedMangas.filter {
|
val mangas = presenter.selection.filter {
|
||||||
it.isEhBasedManga() ||
|
it.isEhBasedManga() ||
|
||||||
it.source in nHentaiSourceIds ||
|
it.source in nHentaiSourceIds ||
|
||||||
it.source == PERV_EDEN_EN_SOURCE_ID ||
|
it.source == PERV_EDEN_EN_SOURCE_ID ||
|
||||||
@ -704,7 +648,7 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pushToMdList() {
|
private fun pushToMdList() {
|
||||||
val mangas = selectedMangas.filter {
|
val mangas = presenter.selection.filter {
|
||||||
it.source in mangaDexSourceIds
|
it.source in mangaDexSourceIds
|
||||||
}
|
}
|
||||||
presenter.syncMangaToDex(mangas)
|
presenter.syncMangaToDex(mangas)
|
||||||
@ -746,21 +690,18 @@ class LibraryController(
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun selectAllCategoryManga() {
|
private fun selectAllCategoryManga() {
|
||||||
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
|
presenter.selectAll(binding.libraryPager.currentItem)
|
||||||
selectAllRelay.call(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectInverseCategoryManga() {
|
private fun selectInverseCategoryManga() {
|
||||||
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
|
presenter.invertSelection(binding.libraryPager.currentItem)
|
||||||
selectInverseRelay.call(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearchViewQueryTextChange(newText: String?) {
|
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||||
// Ignore events if this controller isn't at the top to avoid query being reset
|
// Ignore events if this controller isn't at the top to avoid query being reset
|
||||||
if (router.backstack.lastOrNull()?.controller == this) {
|
if (router.backstack.lastOrNull()?.controller == this) {
|
||||||
presenter.query = newText ?: ""
|
presenter.query = newText ?: ""
|
||||||
|
presenter.searchQuery = newText ?: ""
|
||||||
performSearch()
|
performSearch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -816,7 +757,7 @@ class LibraryController(
|
|||||||
?.setMessage(activity!!.getString(R.string.favorites_sync_bad_library_state, status.message))
|
?.setMessage(activity!!.getString(R.string.favorites_sync_bad_library_state, status.message))
|
||||||
?.setCancelable(false)
|
?.setCancelable(false)
|
||||||
?.setPositiveButton(R.string.show_gallery) { _, _ ->
|
?.setPositiveButton(R.string.show_gallery) { _, _ ->
|
||||||
openManga(status.manga)
|
openManga(status.manga.id)
|
||||||
presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!)
|
presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!)
|
||||||
}
|
}
|
||||||
?.setNegativeButton(android.R.string.ok) { _, _ ->
|
?.setNegativeButton(android.R.string.ok) { _, _ ->
|
||||||
@ -877,11 +818,7 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startReading(manga: Manga, adapter: LibraryCategoryAdapter) {
|
private fun startReading(manga: Manga) {
|
||||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
|
||||||
toggleSelection(manga)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val chapter = presenter.getFirstUnread(manga) ?: return
|
val chapter = presenter.getFirstUnread(manga) ?: return
|
||||||
val intent = ReaderActivity.newIntent(activity, manga.id, chapter.id)
|
val intent = ReaderActivity.newIntent(activity, manga.id, chapter.id)
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic class used to hold the displayed data of a manga in the library.
|
|
||||||
* @param view the inflated view for this holder.
|
|
||||||
* @param adapter the adapter handling this holder.
|
|
||||||
* @param listener a listener to react to the single tap and long tap events.
|
|
||||||
*/
|
|
||||||
|
|
||||||
abstract class LibraryHolder<VB : ViewBinding>(
|
|
||||||
view: View,
|
|
||||||
val adapter: FlexibleAdapter<*>,
|
|
||||||
) : FlexibleViewHolder(view, adapter) {
|
|
||||||
|
|
||||||
abstract val binding: VB
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
|
||||||
* holder with the given manga.
|
|
||||||
*
|
|
||||||
* @param item the manga item to bind.
|
|
||||||
*/
|
|
||||||
abstract fun onSetValues(item: LibraryItem)
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
override fun onLongClick(view: View?): Boolean {
|
|
||||||
return if (adapter.isLongPressDragEnabled) {
|
|
||||||
super.onLongClick(view)
|
|
||||||
false
|
|
||||||
} else super.onLongClick(view)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
|
@ -1,27 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.fredporciuncula.flow.preferences.Preference
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
|
||||||
import eu.davidea.flexibleadapter.items.IFilterable
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
|
||||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class LibraryItem(
|
class LibraryItem(
|
||||||
val manga: LibraryManga,
|
val manga: LibraryManga,
|
||||||
private val shouldSetFromCategory: Preference<Boolean>,
|
) {
|
||||||
private val defaultLibraryDisplayMode: Preference<DisplayModeSetting>,
|
|
||||||
) :
|
|
||||||
AbstractFlexibleItem<LibraryHolder<*>>(), IFilterable<String> {
|
|
||||||
|
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
|
|
||||||
@ -35,63 +21,13 @@ class LibraryItem(
|
|||||||
var startReadingButton = false
|
var startReadingButton = false
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun getDisplayMode(): DisplayModeSetting {
|
|
||||||
return if (shouldSetFromCategory.get() && manga.category != 0) {
|
|
||||||
DisplayModeSetting.fromFlag(displayMode)
|
|
||||||
} else {
|
|
||||||
defaultLibraryDisplayMode.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
|
||||||
return when (getDisplayMode()) {
|
|
||||||
DisplayModeSetting.COMPACT_GRID, DisplayModeSetting.COVER_ONLY_GRID -> R.layout.source_compact_grid_item
|
|
||||||
DisplayModeSetting.COMFORTABLE_GRID -> R.layout.source_comfortable_grid_item
|
|
||||||
DisplayModeSetting.LIST -> R.layout.source_list_item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder<*> {
|
|
||||||
return when (getDisplayMode()) {
|
|
||||||
DisplayModeSetting.COMPACT_GRID -> {
|
|
||||||
LibraryCompactGridHolder(SourceCompactGridItemBinding.bind(view), adapter, coverOnly = false)
|
|
||||||
}
|
|
||||||
DisplayModeSetting.COVER_ONLY_GRID -> {
|
|
||||||
LibraryCompactGridHolder(SourceCompactGridItemBinding.bind(view), adapter, coverOnly = true)
|
|
||||||
}
|
|
||||||
DisplayModeSetting.COMFORTABLE_GRID -> {
|
|
||||||
LibraryComfortableGridHolder(SourceComfortableGridItemBinding.bind(view), adapter)
|
|
||||||
}
|
|
||||||
DisplayModeSetting.LIST -> {
|
|
||||||
LibraryListHolder(view, adapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindViewHolder(
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
holder: LibraryHolder<*>,
|
|
||||||
position: Int,
|
|
||||||
payloads: List<Any?>?,
|
|
||||||
) {
|
|
||||||
holder.onSetValues(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
/**
|
|
||||||
* Returns true if this item is draggable.
|
|
||||||
*/
|
|
||||||
override fun isDraggable(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters a manga depending on a query.
|
* Filters a manga depending on a query.
|
||||||
*
|
*
|
||||||
* @param constraint the query to apply.
|
* @param constraint the query to apply.
|
||||||
* @return true if the manga should be included, false otherwise.
|
* @return true if the manga should be included, false otherwise.
|
||||||
*/
|
*/
|
||||||
override fun filter(constraint: String): Boolean {
|
fun filter(constraint: String): Boolean {
|
||||||
val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
|
val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
|
||||||
val genres by lazy { manga.getGenres() }
|
val genres by lazy { manga.getGenres() }
|
||||||
return manga.title.contains(constraint, true) ||
|
return manga.title.contains(constraint, true) ||
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import coil.dispose
|
|
||||||
import coil.load
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
|
||||||
* All the elements from the layout file "item_library_list" are available in this class.
|
|
||||||
*
|
|
||||||
* @param view the inflated view for this holder.
|
|
||||||
* @param adapter the adapter handling this holder.
|
|
||||||
* @param listener a listener to react to single tap and long tap events.
|
|
||||||
* @constructor creates a new library holder.
|
|
||||||
*/
|
|
||||||
class LibraryListHolder(
|
|
||||||
private val view: View,
|
|
||||||
adapter: FlexibleAdapter<*>,
|
|
||||||
) : LibraryHolder<SourceListItemBinding>(view, adapter) {
|
|
||||||
|
|
||||||
override val binding = SourceListItemBinding.bind(view)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
|
||||||
* holder with the given manga.
|
|
||||||
*
|
|
||||||
* @param item the manga item to bind.
|
|
||||||
*/
|
|
||||||
override fun onSetValues(item: LibraryItem) {
|
|
||||||
// Update the title of the manga.
|
|
||||||
binding.title.text = item.manga.title
|
|
||||||
|
|
||||||
// For rounded corners
|
|
||||||
binding.badges.clipToOutline = true
|
|
||||||
|
|
||||||
// Update the unread count and its visibility.
|
|
||||||
with(binding.unreadText) {
|
|
||||||
isVisible = item.unreadCount > 0
|
|
||||||
text = item.unreadCount.toString()
|
|
||||||
}
|
|
||||||
// Update the download count and its visibility.
|
|
||||||
with(binding.downloadText) {
|
|
||||||
isVisible = item.downloadCount > 0
|
|
||||||
text = "${item.downloadCount}"
|
|
||||||
}
|
|
||||||
// Update the source language and its visibility
|
|
||||||
with(binding.languageText) {
|
|
||||||
isVisible = item.sourceLanguage.isNotEmpty()
|
|
||||||
text = item.sourceLanguage
|
|
||||||
}
|
|
||||||
// show local text badge if local manga
|
|
||||||
binding.localText.isVisible = item.isLocal
|
|
||||||
|
|
||||||
// Create thumbnail onclick to simulate long click
|
|
||||||
binding.thumbnail.setOnClickListener {
|
|
||||||
// Simulate long click on this view to enter selection mode
|
|
||||||
onLongClick(itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the cover
|
|
||||||
binding.thumbnail.dispose()
|
|
||||||
binding.thumbnail.load(item.manga)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.core.util.asObservable
|
import eu.kanade.core.util.asObservable
|
||||||
import eu.kanade.data.DatabaseHandler
|
import eu.kanade.data.DatabaseHandler
|
||||||
@ -13,8 +22,11 @@ import eu.kanade.domain.chapter.interactor.SetReadStatus
|
|||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||||
import eu.kanade.domain.chapter.model.Chapter
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
import eu.kanade.domain.chapter.model.toDbChapter
|
import eu.kanade.domain.chapter.model.toDbChapter
|
||||||
|
import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
|
||||||
import eu.kanade.domain.manga.interactor.GetLibraryManga
|
import eu.kanade.domain.manga.interactor.GetLibraryManga
|
||||||
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
||||||
|
import eu.kanade.domain.manga.interactor.GetSearchTags
|
||||||
|
import eu.kanade.domain.manga.interactor.GetSearchTitles
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaUpdate
|
import eu.kanade.domain.manga.model.MangaUpdate
|
||||||
@ -22,6 +34,7 @@ import eu.kanade.domain.manga.model.isLocal
|
|||||||
import eu.kanade.domain.track.interactor.GetTracks
|
import eu.kanade.domain.track.interactor.GetTracks
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
@ -33,20 +46,37 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||||
import exh.favorites.FavoritesSyncHelper
|
import exh.favorites.FavoritesSyncHelper
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.metadata.sql.models.SearchTag
|
||||||
|
import exh.metadata.sql.models.SearchTitle
|
||||||
|
import exh.search.Namespace
|
||||||
|
import exh.search.QueryComponent
|
||||||
|
import exh.search.SearchEngine
|
||||||
|
import exh.search.Text
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.isEhBasedManga
|
import exh.source.isEhBasedManga
|
||||||
|
import exh.source.isMetadataSource
|
||||||
|
import exh.util.cancellable
|
||||||
import exh.util.isLewd
|
import exh.util.isLewd
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -88,9 +118,13 @@ class LibraryPresenter(
|
|||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
// SY -->
|
// SY -->
|
||||||
|
private val searchEngine: SearchEngine = SearchEngine(),
|
||||||
private val customMangaManager: CustomMangaManager = Injekt.get(),
|
private val customMangaManager: CustomMangaManager = Injekt.get(),
|
||||||
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
|
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
|
||||||
private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(),
|
private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(),
|
||||||
|
private val getIdsOfFavoriteMangaWithMetadata: GetIdsOfFavoriteMangaWithMetadata = Injekt.get(),
|
||||||
|
private val getSearchTags: GetSearchTags = Injekt.get(),
|
||||||
|
private val getSearchTitles: GetSearchTitles = Injekt.get(),
|
||||||
// SY <--
|
// SY <--
|
||||||
) : BasePresenter<LibraryController>() {
|
) : BasePresenter<LibraryController>() {
|
||||||
|
|
||||||
@ -99,9 +133,24 @@ class LibraryPresenter(
|
|||||||
/**
|
/**
|
||||||
* Categories of the library.
|
* Categories of the library.
|
||||||
*/
|
*/
|
||||||
var categories: List<Category> = emptyList()
|
var categories: List<Category> = mutableStateListOf()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var loadedManga = mutableStateMapOf<Long, List<LibraryItem>>()
|
||||||
|
private set
|
||||||
|
|
||||||
|
val loadedMangaFlow = MutableStateFlow(loadedManga)
|
||||||
|
|
||||||
|
var searchQuery by mutableStateOf(query)
|
||||||
|
|
||||||
|
val selection: MutableList<LibraryManga> = mutableStateListOf()
|
||||||
|
|
||||||
|
val isPerCategory by mutableStateOf(preferences.categorizedDisplaySettings().get())
|
||||||
|
|
||||||
|
var columns by mutableStateOf(0)
|
||||||
|
|
||||||
|
var currentDisplayMode by mutableStateOf(preferences.libraryDisplayMode().get())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay used to apply the UI filters to the last emission of the library.
|
* Relay used to apply the UI filters to the last emission of the library.
|
||||||
*/
|
*/
|
||||||
@ -132,6 +181,10 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
|
private val services = trackManager.services.associate { service ->
|
||||||
|
service.id to context.getString(service.nameRes())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay used to apply the UI update to the last emission of the library.
|
* Relay used to apply the UI update to the last emission of the library.
|
||||||
*/
|
*/
|
||||||
@ -145,6 +198,14 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
preferences.libraryDisplayMode()
|
||||||
|
.asFlow()
|
||||||
|
.drop(1)
|
||||||
|
.onEach {
|
||||||
|
currentDisplayMode = it
|
||||||
|
}
|
||||||
|
.launchIn(presenterScope)
|
||||||
|
|
||||||
subscribeLibrary()
|
subscribeLibrary()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,11 +628,7 @@ class LibraryPresenter(
|
|||||||
.map { list ->
|
.map { list ->
|
||||||
list.map { libraryManga ->
|
list.map { libraryManga ->
|
||||||
// Display mode based on user preference: take it from global library setting or category
|
// Display mode based on user preference: take it from global library setting or category
|
||||||
LibraryItem(
|
LibraryItem(libraryManga)
|
||||||
libraryManga,
|
|
||||||
shouldSetFromCategory,
|
|
||||||
defaultLibraryDisplayMode,
|
|
||||||
)
|
|
||||||
}.groupBy { it.manga.category.toLong() }
|
}.groupBy { it.manga.category.toLong() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,7 +768,7 @@ class LibraryPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun cleanTitles(mangas: List<Manga>) {
|
fun cleanTitles(mangas: List<DbManga>) {
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let {
|
val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let {
|
||||||
if (it.contains("|")) {
|
if (it.contains("|")) {
|
||||||
@ -724,18 +781,18 @@ class LibraryPresenter(
|
|||||||
val mangaJson = CustomMangaManager.MangaJson(
|
val mangaJson = CustomMangaManager.MangaJson(
|
||||||
id = manga.id,
|
id = manga.id,
|
||||||
title = editedTitle.nullIfBlank(),
|
title = editedTitle.nullIfBlank(),
|
||||||
author = manga.author.takeUnless { it == manga.ogAuthor },
|
author = manga.author.takeUnless { it == manga.originalAuthor },
|
||||||
artist = manga.artist.takeUnless { it == manga.ogArtist },
|
artist = manga.artist.takeUnless { it == manga.originalArtist },
|
||||||
description = manga.description.takeUnless { it == manga.ogDescription },
|
description = manga.description.takeUnless { it == manga.originalDescription },
|
||||||
genre = manga.genre.takeUnless { it == manga.ogGenre },
|
genre = manga.getGenres().takeUnless { it == manga.getOriginalGenres() },
|
||||||
status = manga.status.takeUnless { it == manga.ogStatus }?.toLong(),
|
status = manga.status.takeUnless { it == manga.originalStatus }?.toLong(),
|
||||||
)
|
)
|
||||||
|
|
||||||
customMangaManager.saveMangaInfo(mangaJson)
|
customMangaManager.saveMangaInfo(mangaJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncMangaToDex(mangaList: List<Manga>) {
|
fun syncMangaToDex(mangaList: List<DbManga>) {
|
||||||
launchIO {
|
launchIO {
|
||||||
MdUtil.getEnabledMangaDex(preferences, sourceManager)?.let { mdex ->
|
MdUtil.getEnabledMangaDex(preferences, sourceManager)?.let { mdex ->
|
||||||
mangaList.forEach {
|
mangaList.forEach {
|
||||||
@ -821,6 +878,173 @@ class LibraryPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
@Composable
|
||||||
|
fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State<List<LibraryItem>> {
|
||||||
|
val unfiltered = loadedManga[categoryId] ?: emptyList()
|
||||||
|
|
||||||
|
return produceState(initialValue = unfiltered, searchQuery) {
|
||||||
|
val query = searchQuery
|
||||||
|
value = withIOContext {
|
||||||
|
if (unfiltered.isNotEmpty() && query.isNotBlank()) {
|
||||||
|
// Prepare filter object
|
||||||
|
val parsedQuery = searchEngine.parseQuery(query)
|
||||||
|
val mangaWithMetaIds = getIdsOfFavoriteMangaWithMetadata.await()
|
||||||
|
unfiltered.asFlow().cancellable().filter { item ->
|
||||||
|
if (isMetadataSource(item.manga.source)) {
|
||||||
|
val mangaId = item.manga.id ?: -1
|
||||||
|
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
||||||
|
// No meta? Filter using title
|
||||||
|
filterManga(parsedQuery, item.manga)
|
||||||
|
} else {
|
||||||
|
val tags = getSearchTags.await(mangaId)
|
||||||
|
val titles = getSearchTitles.await(mangaId)
|
||||||
|
filterManga(parsedQuery, item.manga, false, tags, titles)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterManga(parsedQuery, item.manga)
|
||||||
|
}
|
||||||
|
}.toList()
|
||||||
|
} else {
|
||||||
|
unfiltered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun filterManga(
|
||||||
|
queries: List<QueryComponent>,
|
||||||
|
manga: LibraryManga,
|
||||||
|
checkGenre: Boolean = true,
|
||||||
|
searchTags: List<SearchTag>? = null,
|
||||||
|
searchTitles: List<SearchTitle>? = null,
|
||||||
|
): Boolean {
|
||||||
|
val mappedQueries = queries.groupBy { it.excluded }
|
||||||
|
val tracks = if (loggedServices.isNotEmpty()) getTracks.await(manga.id!!).toList() else null
|
||||||
|
val source = sourceManager.get(manga.source)
|
||||||
|
val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList()
|
||||||
|
val hasNormalQuery = mappedQueries[false]?.all { queryComponent ->
|
||||||
|
when (queryComponent) {
|
||||||
|
is Text -> {
|
||||||
|
val query = queryComponent.asQuery()
|
||||||
|
manga.title.contains(query, true) ||
|
||||||
|
(manga.author?.contains(query, true) == true) ||
|
||||||
|
(manga.artist?.contains(query, true) == true) ||
|
||||||
|
(manga.description?.contains(query, true) == true) ||
|
||||||
|
(source?.name?.contains(query, true) == true) ||
|
||||||
|
(loggedServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) ||
|
||||||
|
(genre.any { it.contains(query, true) }) ||
|
||||||
|
(searchTags.orEmpty().any { it.name.contains(query, true) }) ||
|
||||||
|
(searchTitles.orEmpty().any { it.title.contains(query, true) })
|
||||||
|
}
|
||||||
|
is Namespace -> {
|
||||||
|
searchTags != null && searchTags.any {
|
||||||
|
val tag = queryComponent.tag
|
||||||
|
(it.namespace != null && it.namespace.contains(queryComponent.namespace, true) && tag != null && it.name.contains(tag.asQuery(), true)) ||
|
||||||
|
(tag == null && it.namespace != null && it.namespace.contains(queryComponent.namespace, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val doesNotHaveExcludedQuery = mappedQueries[true]?.all { queryComponent ->
|
||||||
|
when (queryComponent) {
|
||||||
|
is Text -> {
|
||||||
|
val query = queryComponent.asQuery()
|
||||||
|
query.isBlank() || (
|
||||||
|
(!manga.title.contains(query, true)) &&
|
||||||
|
(!manga.author.orEmpty().contains(query, true)) &&
|
||||||
|
(!manga.artist.orEmpty().contains(query, true)) &&
|
||||||
|
(!manga.description.orEmpty().contains(query, true)) &&
|
||||||
|
(!source?.name.orEmpty().contains(query, true)) &&
|
||||||
|
(loggedServices.isEmpty() || loggedServices.isNotEmpty() && tracks == null || tracks != null && !filterTracks(query, tracks)) &&
|
||||||
|
(genre.none { it.contains(query, true) }) &&
|
||||||
|
(searchTags.orEmpty().none { it.name.contains(query, true) }) &&
|
||||||
|
(searchTitles.orEmpty().none { it.title.contains(query, true) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Namespace -> {
|
||||||
|
val searchedTag = queryComponent.tag?.asQuery()
|
||||||
|
searchTags == null || searchTags.all { mangaTag ->
|
||||||
|
if (searchedTag == null || searchedTag.isBlank()) {
|
||||||
|
mangaTag.namespace == null || !mangaTag.namespace.contains(queryComponent.namespace, true)
|
||||||
|
} else if (mangaTag.namespace == null) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
!(mangaTag.name.contains(searchedTag, true) && mangaTag.namespace.contains(queryComponent.namespace, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (hasNormalQuery != null && doesNotHaveExcludedQuery != null && hasNormalQuery && doesNotHaveExcludedQuery) ||
|
||||||
|
(hasNormalQuery != null && doesNotHaveExcludedQuery == null && hasNormalQuery) ||
|
||||||
|
(hasNormalQuery == null && doesNotHaveExcludedQuery != null && doesNotHaveExcludedQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterTracks(constraint: String, tracks: List<eu.kanade.domain.track.model.Track>): Boolean {
|
||||||
|
return tracks.any {
|
||||||
|
val trackService = trackManager.getService(it.syncId)
|
||||||
|
if (trackService != null) {
|
||||||
|
val status = trackService.getStatus(it.status.toInt())
|
||||||
|
val name = services[it.syncId]
|
||||||
|
return@any status.contains(constraint, true) || name?.contains(constraint, true) == true
|
||||||
|
}
|
||||||
|
return@any false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getDisplayMode(index: Int): DisplayModeSetting {
|
||||||
|
val category = categories[index]
|
||||||
|
return remember {
|
||||||
|
if (isPerCategory.not() || category.id == 0L) {
|
||||||
|
currentDisplayMode
|
||||||
|
} else {
|
||||||
|
DisplayModeSetting.fromFlag(category.displayMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasSelection(): Boolean {
|
||||||
|
return selection.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelection() {
|
||||||
|
selection.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSelection(manga: LibraryManga) {
|
||||||
|
if (selection.fastAny { it.id == manga.id }) {
|
||||||
|
selection.remove(manga)
|
||||||
|
} else {
|
||||||
|
selection.add(manga)
|
||||||
|
}
|
||||||
|
view?.invalidateActionMode()
|
||||||
|
view?.createActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectAll(index: Int) {
|
||||||
|
val category = categories[index]
|
||||||
|
val items = loadedManga[category.id] ?: emptyList()
|
||||||
|
selection.addAll(items.filterNot { it.manga in selection }.map { it.manga })
|
||||||
|
view?.createActionModeIfNeeded()
|
||||||
|
view?.invalidateActionMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invertSelection(index: Int) {
|
||||||
|
val category = categories[index]
|
||||||
|
val items = (loadedManga[category.id] ?: emptyList()).map { it.manga }
|
||||||
|
val invert = items.filterNot { it in selection }
|
||||||
|
selection.removeAll(items)
|
||||||
|
selection.addAll(invert)
|
||||||
|
view?.createActionModeIfNeeded()
|
||||||
|
view?.invalidateActionMode()
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
/** Returns first unread chapter of a manga */
|
/** Returns first unread chapter of a manga */
|
||||||
fun getFirstUnread(manga: Manga): Chapter? {
|
fun getFirstUnread(manga: Manga): Chapter? {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
|
|
||||||
sealed class LibrarySelectionEvent {
|
|
||||||
class Selected(val manga: Manga) : LibrarySelectionEvent()
|
|
||||||
class Unselected(val manga: Manga) : LibrarySelectionEvent()
|
|
||||||
object Cleared : LibrarySelectionEvent()
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<eu.kanade.tachiyomi.ui.library.LibraryCategoryView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
|
|
||||||
android:id="@+id/swipe_refresh"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
|
||||||
android:id="@+id/fast_scroller"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
app:fastScrollerBubbleEnabled="false"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</eu.kanade.tachiyomi.ui.library.LibraryCategoryView>
|
|
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<eu.kanade.tachiyomi.widget.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/library_grid"
|
|
||||||
style="@style/Widget.Tachiyomi.GridView.Source"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:columnWidth="140dp"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingTop="5dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
|
||||||
tools:listitem="@layout/source_compact_grid_item" />
|
|
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<eu.kanade.tachiyomi.widget.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/library_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
|
||||||
tools:listitem="@layout/source_list_item" />
|
|
Loading…
x
Reference in New Issue
Block a user