mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 16:21:49 +01:00
Use Compose for Library list and grid (#7520)
This commit is contained in:
parent
018ca71336
commit
905c96922b
@ -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,82 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
LazyLibraryGrid(
|
||||||
|
columns = columns,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryComfortableGridItem(
|
||||||
|
libraryItem,
|
||||||
|
libraryItem.manga in selection,
|
||||||
|
onClick,
|
||||||
|
onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryComfortableGridItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (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,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
maxLines = 2,
|
||||||
|
style = LocalTextStyle.current.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
LazyLibraryGrid(
|
||||||
|
columns = columns,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryCompactGridItem(
|
||||||
|
item = libraryItem,
|
||||||
|
isSelected = libraryItem.manga in selection,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryCompactGridItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
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,68 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
LazyLibraryGrid(
|
||||||
|
columns = columns,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = {
|
||||||
|
it.manga.id!!
|
||||||
|
},
|
||||||
|
) { libraryItem ->
|
||||||
|
LibraryCoverOnlyGridItem(
|
||||||
|
item = libraryItem,
|
||||||
|
isSelected = libraryItem.manga in selection,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryCoverOnlyGridItem(
|
||||||
|
item: LibraryItem,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (LibraryManga) -> Unit,
|
||||||
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
package eu.kanade.presentation.library.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
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,
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BadgeGroup(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,15 @@ 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,
|
||||||
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 +60,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 +89,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 +100,89 @@ 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisplayModeSetting.COMFORTABLE_GRID -> {
|
||||||
|
LibraryComfortableGrid(
|
||||||
|
items = mangaList,
|
||||||
|
columns = presenter.columns,
|
||||||
|
selection = presenter.selection,
|
||||||
|
onClick = onClickManga,
|
||||||
|
onLongClick = onLongClickManga,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisplayModeSetting.COVER_ONLY_GRID -> {
|
||||||
|
LibraryCoverOnlyGrid(
|
||||||
|
items = mangaList,
|
||||||
|
columns = presenter.columns,
|
||||||
|
selection = presenter.selection,
|
||||||
|
onClick = onClickManga,
|
||||||
|
onLongClick = onLongClickManga,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
boundViews.add(view)
|
boundViews.add(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +193,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 +219,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,44 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter storing a list of manga in a certain category.
|
|
||||||
*
|
|
||||||
* @param view the fragment containing this adapter.
|
|
||||||
*/
|
|
||||||
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|
||||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
fun setItems(list: List<LibraryItem>) {
|
|
||||||
// A copy of manga always unfiltered.
|
|
||||||
mangas = list.toList()
|
|
||||||
|
|
||||||
performFilter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 performFilter() {
|
|
||||||
val s = getFilter(String::class.java) ?: ""
|
|
||||||
updateDataSet(mangas.filter { it.filter(s) })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,328 +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 dev.chrisbanes.insetter.applyInsetter
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
|
||||||
import eu.kanade.domain.category.model.Category
|
|
||||||
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.databinding.LibraryCategoryBinding
|
|
||||||
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 kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
|
||||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
|
||||||
import rx.subscriptions.CompositeSubscription
|
|
||||||
import java.util.ArrayDeque
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
|
|
||||||
private val scope = MainScope()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if (LibraryUpdateService.start(context, category)) {
|
|
||||||
context.toast(R.string.updating_category)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions += controller.searchRelay
|
|
||||||
.doOnNext { adapter.setFilter(it) }
|
|
||||||
.skip(1)
|
|
||||||
.subscribe { adapter.performFilter() }
|
|
||||||
|
|
||||||
subscriptions += controller.libraryMangaRelay
|
|
||||||
.subscribe { onNextLibraryManga(it) }
|
|
||||||
|
|
||||||
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() {
|
|
||||||
adapter.setItems(emptyList())
|
|
||||||
adapter.clearSelection()
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDestroy() {
|
|
||||||
unsubscribe()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fun onNextLibraryManga(event: LibraryMangaEvent) {
|
|
||||||
// Get the manga list for this category.
|
|
||||||
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
|
||||||
|
|
||||||
// Update the category with its manga.
|
|
||||||
adapter.setItems(mangaForCategory)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is LibrarySelectionEvent.Cleared -> {
|
|
||||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
|
||||||
adapter.clearSelection()
|
|
||||||
|
|
||||||
lastClickPositionStack.clear()
|
|
||||||
lastClickPositionStack.push(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()!!
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,61 +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.databinding.SourceComfortableGridItemBinding
|
|
||||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.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
|
|
||||||
|
|
||||||
// Update the cover.
|
|
||||||
binding.thumbnail.dispose()
|
|
||||||
binding.thumbnail.loadAutoPause(item.manga)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import coil.dispose
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
|
||||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.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
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,16 +14,15 @@ import com.bluelinelabs.conductor.ControllerChangeType
|
|||||||
import com.fredporciuncula.flow.preferences.Preference
|
import com.fredporciuncula.flow.preferences.Preference
|
||||||
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.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.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
|
||||||
@ -33,7 +32,6 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
|
|||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
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
|
||||||
@ -73,42 +71,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.
|
||||||
*/
|
*/
|
||||||
@ -174,7 +141,19 @@ 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!!)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
@ -185,13 +164,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +192,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) {
|
||||||
@ -229,7 +210,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
|
||||||
@ -313,6 +293,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))
|
||||||
|
|
||||||
@ -320,19 +306,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()
|
||||||
@ -400,7 +373,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 =
|
||||||
@ -455,7 +427,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()
|
||||||
@ -466,9 +438,9 @@ 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 } } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
@ -487,50 +459,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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -538,8 +478,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,15 +488,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
|
||||||
@ -566,26 +505,27 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
|
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
|
||||||
@ -599,21 +539,18 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +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,
|
|
||||||
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)
|
|
||||||
}
|
|
@ -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()
|
||||||
|
|
||||||
@ -31,55 +17,13 @@ class LibraryItem(
|
|||||||
var isLocal = false
|
var isLocal = false
|
||||||
var sourceLanguage = ""
|
var sourceLanguage = ""
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,
|
|
||||||
private val 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.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
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
|
||||||
@ -18,6 +27,7 @@ import eu.kanade.domain.manga.model.MangaUpdate
|
|||||||
import eu.kanade.domain.manga.model.isLocal
|
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.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.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -26,6 +36,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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.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
|
||||||
@ -33,6 +44,12 @@ 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.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 kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -80,9 +97,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.
|
||||||
*/
|
*/
|
||||||
@ -105,6 +137,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,11 +456,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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -592,4 +628,68 @@ class LibraryPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State<List<LibraryItem>> {
|
||||||
|
val unfiltered = loadedManga[categoryId] ?: emptyList()
|
||||||
|
|
||||||
|
return derivedStateOf {
|
||||||
|
val query = searchQuery
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
unfiltered.filter {
|
||||||
|
it.filter(query)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unfiltered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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…
Reference in New Issue
Block a user