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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
@ -17,6 +20,15 @@ import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
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.clickableNoIndication(
|
||||
|
@ -3,14 +3,34 @@ package eu.kanade.tachiyomi.ui.library
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.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.databinding.LibraryCategoryBinding
|
||||
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
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.api.get
|
||||
|
||||
@ -21,13 +41,15 @@ import uy.kohesive.injekt.api.get
|
||||
*/
|
||||
class LibraryAdapter(
|
||||
private val controller: LibraryController,
|
||||
private val presenter: LibraryPresenter,
|
||||
private val onClickManga: (LibraryManga) -> Unit,
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
) : RecyclerViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* The categories to bind in the adapter.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
var categories: List<Category> = mutableStateListOf()
|
||||
private set
|
||||
|
||||
/**
|
||||
@ -38,19 +60,6 @@ class LibraryAdapter(
|
||||
|
||||
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
|
||||
*/
|
||||
@ -80,10 +89,8 @@ class LibraryAdapter(
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun inflateView(container: ViewGroup, viewType: Int): View {
|
||||
val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
|
||||
val view: LibraryCategoryView = binding.root
|
||||
view.onCreate(controller, binding, viewType)
|
||||
return view
|
||||
val binding = ComposeControllerBinding.inflate(LayoutInflater.from(container.context), container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,7 +100,89 @@ class LibraryAdapter(
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
@ -104,7 +193,6 @@ class LibraryAdapter(
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun recycleView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onRecycle()
|
||||
boundViews.remove(view)
|
||||
}
|
||||
|
||||
@ -131,45 +219,5 @@ class LibraryAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
override fun getViewType(position: Int): Int = -1
|
||||
}
|
||||
|
@ -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.google.android.material.tabs.TabLayout
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.toDbCategory
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
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.PreferencesHelper
|
||||
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.SearchableNucleusController
|
||||
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.util.lang.launchIO
|
||||
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.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@ -73,42 +71,11 @@ class LibraryController(
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -174,7 +141,19 @@ class LibraryController(
|
||||
override fun onViewCreated(view: 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.pageSelections()
|
||||
.drop(1)
|
||||
@ -185,13 +164,7 @@ class LibraryController(
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
getColumnsPreferenceForCurrentOrientation().asImmediateFlow { mangaPerRow = it }
|
||||
.drop(1)
|
||||
// Set again the adapter to recalculate the covers height
|
||||
.onEach { reattachAdapter() }
|
||||
.launchIn(viewScope)
|
||||
|
||||
if (selectedMangas.isNotEmpty()) {
|
||||
if (adapter!!.categories.isNotEmpty()) {
|
||||
createActionModeIfNeeded()
|
||||
}
|
||||
|
||||
@ -219,6 +192,14 @@ class LibraryController(
|
||||
.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) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isEnter) {
|
||||
@ -229,7 +210,6 @@ class LibraryController(
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
destroyActionModeIfNeeded()
|
||||
adapter?.onDestroy()
|
||||
adapter = null
|
||||
settingsSheet?.sheetScope?.cancel()
|
||||
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.
|
||||
libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
|
||||
|
||||
@ -320,19 +306,6 @@ class LibraryController(
|
||||
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() {
|
||||
presenter.requestFilterUpdate()
|
||||
activity?.invalidateOptionsMenu()
|
||||
@ -400,7 +373,6 @@ class LibraryController(
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
searchRelay.call(presenter.query)
|
||||
if (presenter.query.isNotEmpty()) {
|
||||
binding.btnGlobalSearch.isVisible = true
|
||||
binding.btnGlobalSearch.text =
|
||||
@ -455,7 +427,7 @@ class LibraryController(
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = selectedMangas.size
|
||||
val count = presenter.selection.size
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
@ -466,9 +438,9 @@ class LibraryController(
|
||||
}
|
||||
|
||||
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
||||
if (selectedMangas.isEmpty()) return
|
||||
if (presenter.hasSelection().not()) return
|
||||
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 {
|
||||
@ -487,50 +459,18 @@ class LibraryController(
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
// Clear all the manga selections and notify child views.
|
||||
selectedMangas.clear()
|
||||
selectionRelay.call(LibrarySelectionEvent.Cleared)
|
||||
presenter.clearSelection()
|
||||
|
||||
(activity as? MainActivity)?.showBottomNav(true)
|
||||
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
fun openManga(manga: Manga) {
|
||||
fun openManga(mangaId: Long) {
|
||||
// Notify the presenter a manga is being opened.
|
||||
presenter.onOpenManga()
|
||||
|
||||
router.pushController(MangaController(manga.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
router.pushController(MangaController(mangaId))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -538,8 +478,7 @@ class LibraryController(
|
||||
* invalidate the action mode to revert the top toolbar
|
||||
*/
|
||||
fun clearSelection() {
|
||||
selectedMangas.clear()
|
||||
selectionRelay.call(LibrarySelectionEvent.Cleared)
|
||||
presenter.clearSelection()
|
||||
invalidateActionMode()
|
||||
}
|
||||
|
||||
@ -549,15 +488,15 @@ class LibraryController(
|
||||
private fun showMangaCategoriesDialog() {
|
||||
viewScope.launchIO {
|
||||
// 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.
|
||||
val categories = presenter.categories.filter { it.id != 0L }
|
||||
|
||||
// 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.
|
||||
val mix = presenter.getMixCategories(mangas)
|
||||
val mix = presenter.getMixCategories(mangas.mapNotNull { it.toDomainManga() })
|
||||
val preselected = categories.map {
|
||||
when (it) {
|
||||
in common -> QuadStateTextView.State.CHECKED.ordinal
|
||||
@ -566,26 +505,27 @@ class LibraryController(
|
||||
}
|
||||
}.toTypedArray()
|
||||
launchUI {
|
||||
ChangeMangaCategoriesDialog(this@LibraryController, mangas, categories, preselected)
|
||||
ChangeMangaCategoriesDialog(this@LibraryController, mangas.mapNotNull { it.toDomainManga() }, categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadUnreadChapters() {
|
||||
val mangas = selectedMangas.toList()
|
||||
presenter.downloadUnreadChapters(mangas)
|
||||
val mangas = presenter.selection.toList()
|
||||
presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() })
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun markReadStatus(read: Boolean) {
|
||||
val mangas = selectedMangas.toList()
|
||||
presenter.markReadStatus(mangas, read)
|
||||
val mangas = presenter.selection.toList()
|
||||
presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
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>) {
|
||||
@ -599,21 +539,18 @@ class LibraryController(
|
||||
}
|
||||
|
||||
private fun selectAllCategoryManga() {
|
||||
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
|
||||
selectAllRelay.call(it)
|
||||
}
|
||||
presenter.selectAll(binding.libraryPager.currentItem)
|
||||
}
|
||||
|
||||
private fun selectInverseCategoryManga() {
|
||||
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
|
||||
selectInverseRelay.call(it)
|
||||
}
|
||||
presenter.invertSelection(binding.libraryPager.currentItem)
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||
// Ignore events if this controller isn't at the top to avoid query being reset
|
||||
if (router.backstack.lastOrNull()?.controller == this) {
|
||||
presenter.query = newText ?: ""
|
||||
presenter.searchQuery = newText ?: ""
|
||||
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
|
||||
|
||||
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.databinding.SourceComfortableGridItemBinding
|
||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class LibraryItem(
|
||||
val manga: LibraryManga,
|
||||
private val shouldSetFromCategory: Preference<Boolean>,
|
||||
private val defaultLibraryDisplayMode: Preference<DisplayModeSetting>,
|
||||
) :
|
||||
AbstractFlexibleItem<LibraryHolder<*>>(), IFilterable<String> {
|
||||
) {
|
||||
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
|
||||
@ -31,55 +17,13 @@ class LibraryItem(
|
||||
var isLocal = false
|
||||
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.
|
||||
*
|
||||
* @param constraint the query to apply.
|
||||
* @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 genres by lazy { manga.getGenres() }
|
||||
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
|
||||
|
||||
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 eu.kanade.core.util.asObservable
|
||||
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.track.interactor.GetTracks
|
||||
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.download.DownloadManager
|
||||
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.online.HttpSource
|
||||
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.SortModeSetting
|
||||
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.removeCovers
|
||||
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 rx.Observable
|
||||
import rx.Subscription
|
||||
@ -80,9 +97,24 @@ class LibraryPresenter(
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
var categories: List<Category> = mutableStateListOf()
|
||||
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.
|
||||
*/
|
||||
@ -105,6 +137,14 @@ class LibraryPresenter(
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
preferences.libraryDisplayMode()
|
||||
.asFlow()
|
||||
.drop(1)
|
||||
.onEach {
|
||||
currentDisplayMode = it
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
@ -416,11 +456,7 @@ class LibraryPresenter(
|
||||
.map { list ->
|
||||
list.map { libraryManga ->
|
||||
// Display mode based on user preference: take it from global library setting or category
|
||||
LibraryItem(
|
||||
libraryManga,
|
||||
shouldSetFromCategory,
|
||||
defaultLibraryDisplayMode,
|
||||
)
|
||||
LibraryItem(libraryManga)
|
||||
}.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