diff --git a/app/src/main/java/eu/kanade/presentation/library/components/Badge.kt b/app/src/main/java/eu/kanade/presentation/library/components/Badge.kt new file mode 100644 index 000000000..b73001bd0 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/Badge.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LazyLibraryGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LazyLibraryGrid.kt new file mode 100644 index 000000000..90e2d7398 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LazyLibraryGrid.kt @@ -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, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt new file mode 100644 index 000000000..06f556147 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt @@ -0,0 +1,95 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import eu.kanade.domain.manga.model.MangaCover +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.ui.library.LibraryItem + +@Composable +fun LibraryComfortableGrid( + items: List, + columns: Int, + selection: List, + onClick: (LibraryManga) -> Unit, + onLongClick: (LibraryManga) -> Unit, + // SY --> + onOpenReader: (LibraryManga) -> Unit, + // SY <-- +) { + LazyLibraryGrid( + columns = columns, + ) { + items( + items = items, + key = { + it.manga.id!! + }, + ) { libraryItem -> + LibraryComfortableGridItem( + libraryItem, + libraryItem.manga in selection, + onClick, + onLongClick, + // SY --> + onOpenReader, + // SY <-- + ) + } + } +} + +@Composable +fun LibraryComfortableGridItem( + item: LibraryItem, + isSelected: Boolean, + onClick: (LibraryManga) -> Unit, + onLongClick: (LibraryManga) -> Unit, + onOpenReader: (LibraryManga) -> Unit, +) { + val manga = item.manga + LibraryGridItemSelectable(isSelected = isSelected) { + Column( + modifier = Modifier + .combinedClickable( + onClick = { + onClick(manga) + }, + onLongClick = { + onLongClick(manga) + }, + ), + ) { + LibraryGridCover( + mangaCover = MangaCover( + manga.id!!, + manga.source, + manga.favorite, + manga.thumbnail_url, + manga.cover_last_modified, + ), + downloadCount = item.downloadCount, + unreadCount = item.unreadCount, + isLocal = item.isLocal, + language = item.sourceLanguage, + // SY --> + showPlayButton = item.startReadingButton, + onOpenReader = { + onOpenReader(manga) + }, + // SY <-- + ) + Text( + text = manga.title, + maxLines = 2, + style = LocalTextStyle.current.copy(fontWeight = FontWeight.SemiBold), + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt new file mode 100644 index 000000000..dfb2a96a5 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt @@ -0,0 +1,120 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.ui.library.LibraryItem + +@Composable +fun LibraryCompactGrid( + items: List, + columns: Int, + selection: List, + onClick: (LibraryManga) -> Unit, + onLongClick: (LibraryManga) -> Unit, + // SY --> + onOpenReader: (LibraryManga) -> Unit, + // SY <-- +) { + LazyLibraryGrid( + columns = columns, + ) { + items( + items = items, + key = { + it.manga.id!! + }, + ) { libraryItem -> + LibraryCompactGridItem( + item = libraryItem, + isSelected = libraryItem.manga in selection, + onClick = onClick, + onLongClick = onLongClick, + // SY --> + onOpenReader = onOpenReader, + // SY <-- + ) + } + } +} + +@Composable +fun LibraryCompactGridItem( + item: LibraryItem, + isSelected: Boolean, + onClick: (LibraryManga) -> Unit, + onLongClick: (LibraryManga) -> Unit, + // SY --> + onOpenReader: (LibraryManga) -> Unit, + // SY <-- +) { + val manga = item.manga + LibraryGridCover( + modifier = Modifier + .selectedOutline(isSelected) + .combinedClickable( + onClick = { + onClick(manga) + }, + onLongClick = { + onLongClick(manga) + }, + ), + mangaCover = eu.kanade.domain.manga.model.MangaCover( + manga.id!!, + manga.source, + manga.favorite, + manga.thumbnail_url, + manga.cover_last_modified, + ), + downloadCount = item.downloadCount, + unreadCount = item.unreadCount, + isLocal = item.isLocal, + language = item.sourceLanguage, + // SY --> + showPlayButton = item.startReadingButton, + playButtonPosition = PlayButtonPosition.Top, + onOpenReader = { + onOpenReader(manga) + }, + // SY <-- + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) + .background( + Brush.verticalGradient( + 0f to Color.Transparent, + 1f to Color(0xAA000000), + ), + ) + .fillMaxHeight(0.33f) + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + Text( + text = manga.title, + modifier = Modifier + .padding(8.dp) + .align(Alignment.BottomStart), + maxLines = 2, + style = LocalTextStyle.current.copy(color = Color.White, fontWeight = FontWeight.SemiBold), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt new file mode 100644 index 000000000..8c899ef52 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt @@ -0,0 +1,83 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.ui.library.LibraryItem + +@Composable +fun LibraryCoverOnlyGrid( + items: List, + columns: Int, + selection: List, + onClick: (LibraryManga) -> Unit, + onLongClick: (LibraryManga) -> Unit, + // SY --> + onOpenReader: (LibraryManga) -> Unit, + // SY <-- +) { + LazyLibraryGrid( + columns = columns, + ) { + items( + items = items, + key = { + it.manga.id!! + }, + ) { libraryItem -> + LibraryCoverOnlyGridItem( + item = libraryItem, + isSelected = libraryItem.manga in selection, + onClick = onClick, + onLongClick = onLongClick, + // SY --> + onOpenReader = onOpenReader, + // SY <-- + ) + } + } +} + +@Composable +fun LibraryCoverOnlyGridItem( + item: LibraryItem, + isSelected: Boolean, + onClick: (LibraryManga) -> Unit, + onLongClick: (LibraryManga) -> Unit, + // SY --> + onOpenReader: (LibraryManga) -> Unit, + // SY <-- +) { + val manga = item.manga + LibraryGridCover( + modifier = Modifier + .selectedOutline(isSelected) + .combinedClickable( + onClick = { + onClick(manga) + }, + onLongClick = { + onLongClick(manga) + }, + ), + mangaCover = eu.kanade.domain.manga.model.MangaCover( + manga.id!!, + manga.source, + manga.favorite, + manga.thumbnail_url, + manga.cover_last_modified, + ), + downloadCount = item.downloadCount, + unreadCount = item.unreadCount, + isLocal = item.isLocal, + language = item.sourceLanguage, + // SY --> + showPlayButton = item.startReadingButton, + onOpenReader = { + onOpenReader(manga) + }, + // SY <-- + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt new file mode 100644 index 000000000..73f359f57 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt @@ -0,0 +1,99 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.kanade.presentation.components.MangaCover +import eu.kanade.tachiyomi.R + +@Composable +fun LibraryGridCover( + modifier: Modifier = Modifier, + mangaCover: eu.kanade.domain.manga.model.MangaCover, + downloadCount: Int, + unreadCount: Int, + isLocal: Boolean, + language: String, + // SY --> + showPlayButton: Boolean, + playButtonPosition: PlayButtonPosition = PlayButtonPosition.Bottom, + onOpenReader: () -> Unit, + // SY <-- + content: @Composable BoxScope.() -> Unit = {}, +) { + Box( + modifier = modifier + .fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio), + ) { + MangaCover.Book( + modifier = Modifier.fillMaxWidth(), + data = mangaCover, + ) + content() + BadgeGroup( + modifier = Modifier + .padding(4.dp) + .align(Alignment.TopStart), + ) { + if (downloadCount > 0) { + Badge( + text = "$downloadCount", + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + if (unreadCount > 0) { + Badge(text = "$unreadCount") + } + } + // SY --> + Column( + Modifier.align(Alignment.TopEnd), + horizontalAlignment = Alignment.End, + ) { + // SY <-- + BadgeGroup( + modifier = Modifier + .padding(4.dp), + ) { + if (isLocal) { + Badge( + text = stringResource(id = R.string.local_source_badge), + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + if (isLocal.not() && language.isNotEmpty()) { + Badge( + text = language, + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + } + // SY --> + if (showPlayButton && playButtonPosition == PlayButtonPosition.Top) { + StartReadingButton(onOpenReader = onOpenReader) + } + } + if (showPlayButton && playButtonPosition == PlayButtonPosition.Bottom) { + StartReadingButton( + Modifier.align(Alignment.BottomEnd), + onOpenReader = onOpenReader, + ) + } + // SY <-- + } +} + +enum class PlayButtonPosition { Top, Bottom } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridItemSelectable.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridItemSelectable.kt new file mode 100644 index 000000000..4145c9d44 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridItemSelectable.kt @@ -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() + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt new file mode 100644 index 000000000..f2ed0957e --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt @@ -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, + columns: Int, + selection: List, + 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, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/StartReadingButton.kt b/app/src/main/java/eu/kanade/presentation/library/components/StartReadingButton.kt new file mode 100644 index 000000000..05796821d --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/StartReadingButton.kt @@ -0,0 +1,45 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.kanade.tachiyomi.R + +@Composable +fun StartReadingButton( + modifier: Modifier = Modifier, + onOpenReader: () -> Unit, +) { + Box( + modifier then Modifier.size(50.dp) + .clip(CircleShape), + contentAlignment = Alignment.Center, + ) { + Box( + Modifier.size(32.dp) + .clip(CircleShape) + .background(Color(0xAD212121)) + .border(0.1.dp, Color(0xFFEDEDED)) + .clickable(onClick = onOpenReader), + ) + Icon( + painter = painterResource(R.drawable.ic_start_reading_24dp), + contentDescription = stringResource(R.string.action_start_reading), + tint = Color.White, + modifier = Modifier.size(24.dp).padding(3.dp), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/util/Modifier.kt b/app/src/main/java/eu/kanade/presentation/util/Modifier.kt index 3eefaa461..d3ab2c473 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Modifier.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Modifier.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt index 34f983091..e9d33a289 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt @@ -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,18 @@ import uy.kohesive.injekt.api.get */ class LibraryAdapter( private val controller: LibraryController, + private val presenter: LibraryPresenter, + private val onClickManga: (LibraryManga) -> Unit, + // SY --> + private val onOpenReader: (LibraryManga) -> Unit, + // SY <-- private val preferences: PreferencesHelper = Injekt.get(), ) : RecyclerViewPagerAdapter() { /** * The categories to bind in the adapter. */ - var categories: List = emptyList() + var categories: List = mutableStateListOf() private set /** @@ -38,19 +63,6 @@ class LibraryAdapter( private var boundViews = arrayListOf() - 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 +92,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 +103,98 @@ 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, + // SY --> + onOpenReader = onOpenReader, + // SY <-- + ) + } + DisplayModeSetting.COMFORTABLE_GRID -> { + LibraryComfortableGrid( + items = mangaList, + columns = presenter.columns, + selection = presenter.selection, + onClick = onClickManga, + onLongClick = onLongClickManga, + // SY --> + onOpenReader = onOpenReader, + // SY <-- + ) + } + DisplayModeSetting.COVER_ONLY_GRID -> { + LibraryCoverOnlyGrid( + items = mangaList, + columns = presenter.columns, + selection = presenter.selection, + onClick = onClickManga, + onLongClick = onLongClickManga, + // SY --> + onOpenReader = onOpenReader, + // SY <-- + ) + } + } + } + } + } + } + } boundViews.add(view) } @@ -104,7 +205,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 +231,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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt deleted file mode 100755 index a19ba5fa9..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ /dev/null @@ -1,241 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata -import eu.kanade.domain.manga.interactor.GetSearchTags -import eu.kanade.domain.manga.interactor.GetSearchTitles -import eu.kanade.domain.manga.model.Manga -import eu.kanade.domain.track.interactor.GetTracks -import eu.kanade.tachiyomi.data.database.models.LibraryManga -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.util.lang.withUIContext -import exh.log.xLogW -import exh.metadata.sql.models.SearchTag -import exh.metadata.sql.models.SearchTitle -import exh.search.Namespace -import exh.search.QueryComponent -import exh.search.SearchEngine -import exh.search.Text -import exh.source.isMetadataSource -import exh.util.cancellable -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import uy.kohesive.injekt.injectLazy - -/** - * Adapter storing a list of manga in a certain category. - * - * @param view the fragment containing this adapter. - */ -class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryController) : - FlexibleAdapter(null, view, true) { - // EXH --> - private val searchEngine = SearchEngine() - private var lastFilterJob: Job? = null - private val sourceManager: SourceManager by injectLazy() - private val trackManager: TrackManager by injectLazy() - private val preferences: PreferencesHelper by injectLazy() - private val hasLoggedServices by lazy { - trackManager.hasLoggedServices() - } - private val services = trackManager.services.associate { service -> - service.id to controller.activity!!.getString(service.nameRes()) - } - private val getIdsOfFavoriteMangaWithMetadata: GetIdsOfFavoriteMangaWithMetadata by injectLazy() - private val getTracks: GetTracks by injectLazy() - private val getSearchTags: GetSearchTags by injectLazy() - private val getSearchTitles: GetSearchTitles by injectLazy() - - // Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter - var searchText - get() = getFilter(String::class.java).orEmpty() - set(value) { - setFilter(value) - } - // EXH <-- - - /** - * The list of manga in this category. - */ - private var mangas: List = emptyList() - - /** - * Sets a list of manga in the adapter. - * - * @param list the list to set. - */ - suspend fun setItems(scope: CoroutineScope, list: List) { - // A copy of manga always unfiltered. - mangas = list.toList() - - performFilter(scope) - } - - /** - * Returns the position in the adapter for the given manga. - * - * @param manga the manga to find. - */ - fun indexOf(manga: Manga): Int { - return currentItems.indexOfFirst { it.manga.id == manga.id } - } - - fun canDrag() = (mode != Mode.MULTI || (mode == Mode.MULTI && selectedItemCount == 1)) && - searchText.isBlank() && - preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT && - !preferences.downloadedOnly().get() && - preferences.filterDownloaded().get() == Filter.TriState.STATE_IGNORE && - preferences.filterCompleted().get() == Filter.TriState.STATE_IGNORE && - preferences.filterStarted().get() == Filter.TriState.STATE_IGNORE && - preferences.filterUnread().get() == Filter.TriState.STATE_IGNORE && - services.all { preferences.filterTracking(it.key).get() == Filter.TriState.STATE_IGNORE } && - preferences.filterLewd().get() == Filter.TriState.STATE_IGNORE - - // EXH --> - // Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it - // (well technically we can cancel it by invoking filterItems again but that doesn't work when - // we want to perform a no-op filter) - suspend fun performFilter(scope: CoroutineScope) { - isLongPressDragEnabled = canDrag() - lastFilterJob?.cancel() - if (mangas.isNotEmpty() && searchText.isNotBlank()) { - val savedSearchText = searchText - - val job = scope.launch(Dispatchers.IO) { - val newManga = try { - // Prepare filter object - val parsedQuery = searchEngine.parseQuery(savedSearchText) - val mangaWithMetaIds = getIdsOfFavoriteMangaWithMetadata.await() - - ensureActive() // Fail early when cancelled - - // Flow the mangas to allow cancellation of this filter operation - mangas.asFlow().cancellable().filter { item -> - if (isMetadataSource(item.manga.source)) { - val mangaId = item.manga.id ?: -1 - if (mangaWithMetaIds.binarySearch(mangaId) < 0) { - // No meta? Filter using title - filterManga(parsedQuery, item.manga) - } else { - val tags = getSearchTags.await(mangaId) - val titles = getSearchTitles.await(mangaId) - filterManga(parsedQuery, item.manga, false, tags, titles) - } - } else { - filterManga(parsedQuery, item.manga) - } - }.toList() - } catch (e: Exception) { - // Do not catch cancellations - if (e is CancellationException) throw e - - this@LibraryCategoryAdapter.xLogW("Could not filter mangas!", e) - mangas - } - - withUIContext { - updateDataSet(newManga) - } - } - lastFilterJob = job - job.join() - } else { - updateDataSet(mangas) - } - } - - private suspend fun filterManga( - queries: List, - manga: LibraryManga, - checkGenre: Boolean = true, - searchTags: List? = null, - searchTitles: List? = null, - ): Boolean { - val mappedQueries = queries.groupBy { it.excluded } - val tracks = if (hasLoggedServices) getTracks.await(manga.id!!).toList() else null - val source = sourceManager.get(manga.source) - val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList() - val hasNormalQuery = mappedQueries[false]?.all { queryComponent -> - when (queryComponent) { - is Text -> { - val query = queryComponent.asQuery() - manga.title.contains(query, true) || - (manga.author?.contains(query, true) == true) || - (manga.artist?.contains(query, true) == true) || - (manga.description?.contains(query, true) == true) || - (source?.name?.contains(query, true) == true) || - (hasLoggedServices && tracks != null && filterTracks(query, tracks)) || - (genre.any { it.contains(query, true) }) || - (searchTags.orEmpty().any { it.name.contains(query, true) }) || - (searchTitles.orEmpty().any { it.title.contains(query, true) }) - } - is Namespace -> { - searchTags != null && searchTags.any { - val tag = queryComponent.tag - (it.namespace != null && it.namespace.contains(queryComponent.namespace, true) && tag != null && it.name.contains(tag.asQuery(), true)) || - (tag == null && it.namespace != null && it.namespace.contains(queryComponent.namespace, true)) - } - } - else -> true - } - } - val doesNotHaveExcludedQuery = mappedQueries[true]?.all { queryComponent -> - when (queryComponent) { - is Text -> { - val query = queryComponent.asQuery() - query.isBlank() || ( - (!manga.title.contains(query, true)) && - (!manga.author.orEmpty().contains(query, true)) && - (!manga.artist.orEmpty().contains(query, true)) && - (!manga.description.orEmpty().contains(query, true)) && - (!source?.name.orEmpty().contains(query, true)) && - (!hasLoggedServices || hasLoggedServices && tracks == null || tracks != null && !filterTracks(query, tracks)) && - (genre.none { it.contains(query, true) }) && - (searchTags.orEmpty().none { it.name.contains(query, true) }) && - (searchTitles.orEmpty().none { it.title.contains(query, true) }) - ) - } - is Namespace -> { - val searchedTag = queryComponent.tag?.asQuery() - searchTags == null || searchTags.all { mangaTag -> - if (searchedTag == null || searchedTag.isBlank()) { - mangaTag.namespace == null || !mangaTag.namespace.contains(queryComponent.namespace, true) - } else if (mangaTag.namespace == null) { - true - } else { - !(mangaTag.name.contains(searchedTag, true) && mangaTag.namespace.contains(queryComponent.namespace, true)) - } - } - } - else -> true - } - } - - return (hasNormalQuery != null && doesNotHaveExcludedQuery != null && hasNormalQuery && doesNotHaveExcludedQuery) || - (hasNormalQuery != null && doesNotHaveExcludedQuery == null && hasNormalQuery) || - (hasNormalQuery == null && doesNotHaveExcludedQuery != null && doesNotHaveExcludedQuery) - } - - private fun filterTracks(constraint: String, tracks: List): Boolean { - return tracks.any { - val trackService = trackManager.getService(it.syncId) - if (trackService != null) { - val status = trackService.getStatus(it.status.toInt()) - val name = services[it.syncId] - return@any status.contains(constraint, true) || name?.contains(constraint, true) == true - } - return@any false - } - } - // EXH <-- -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt deleted file mode 100755 index c02a4ee73..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ /dev/null @@ -1,491 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.FrameLayout -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.domain.category.interactor.UpdateCategory -import eu.kanade.domain.category.model.Category -import eu.kanade.domain.category.model.CategoryUpdate -import eu.kanade.domain.category.model.toDbCategory -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.toDomainManga -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferenceValues -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding -import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting -import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.lang.plusAssign -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.util.view.onAnimationsFinished -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import exh.ui.LoadingHandle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import reactivecircus.flowbinding.recyclerview.scrollStateChanges -import reactivecircus.flowbinding.swiperefreshlayout.refreshes -import rx.android.schedulers.AndroidSchedulers -import rx.subscriptions.CompositeSubscription -import uy.kohesive.injekt.injectLazy -import java.util.ArrayDeque -import java.util.concurrent.TimeUnit - -/** - * Fragment containing the library manga for a certain category. - */ -class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FrameLayout(context, attrs), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - // SY --> - FlexibleAdapter.OnItemMoveListener { - // SY <-- - - private val scope = MainScope() - - private val preferences: PreferencesHelper by injectLazy() - - /** - * The fragment containing this view. - */ - private lateinit var controller: LibraryController - - /** - * Category for this view. - */ - lateinit var category: Category - private set - - /** - * Recycler view of the list of manga. - */ - private lateinit var recycler: AutofitRecyclerView - - /** - * Adapter to hold the manga in this category. - */ - private lateinit var adapter: LibraryCategoryAdapter - - /** - * Subscriptions while the view is bound. - */ - private var subscriptions = CompositeSubscription() - - private var lastClickPositionStack = ArrayDeque(listOf(-1)) - - // EXH --> - private var initialLoadHandle: LoadingHandle? = null - // EXH <-- - - fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding, viewType: Int) { - this.controller = controller - - recycler = if (viewType == LibraryAdapter.LIST_DISPLAY_MODE) { - (binding.swipeRefresh.inflate(R.layout.library_list_recycler) as AutofitRecyclerView).apply { - spanCount = 1 - } - } else { - (binding.swipeRefresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { - spanCount = controller.mangaPerRow - } - } - - recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = LibraryCategoryAdapter(this, controller) - - recycler.setHasFixedSize(true) - recycler.adapter = adapter - binding.swipeRefresh.addView(recycler) - adapter.fastScroller = binding.fastScroller - - recycler.scrollStateChanges() - .onEach { - // Disable swipe refresh when view is not at the top - val firstPos = (recycler.layoutManager as LinearLayoutManager) - .findFirstCompletelyVisibleItemPosition() - binding.swipeRefresh.isEnabled = firstPos <= 0 - } - .launchIn(scope) - - recycler.onAnimationsFinished { - (controller.activity as? MainActivity)?.ready = true - } - - // Double the distance required to trigger sync - binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) - binding.swipeRefresh.refreshes() - .onEach { - // SY --> - if (LibraryUpdateService.start(context, if (controller.presenter.groupType == LibraryGroup.BY_DEFAULT) category else null, group = controller.presenter.groupType, groupExtra = getGroupExtra())) { - context.toast( - when { - controller.presenter.groupType == LibraryGroup.BY_DEFAULT || - (preferences.groupLibraryUpdateType().get() == PreferenceValues.GroupLibraryMode.ALL) -> R.string.updating_category - ( - controller.presenter.groupType == LibraryGroup.UNGROUPED && - preferences.groupLibraryUpdateType().get() == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED - ) || - preferences.groupLibraryUpdateType().get() == PreferenceValues.GroupLibraryMode.GLOBAL -> R.string.updating_library - else -> R.string.updating_category - }, - ) - } - // SY <-- - - // It can be a very long operation, so we disable swipe refresh and show a toast. - binding.swipeRefresh.isRefreshing = false - } - .launchIn(scope) - } - - fun onBind(category: Category) { - this.category = category - - adapter.mode = if (controller.selectedMangas.isNotEmpty()) { - SelectableAdapter.Mode.MULTI - } else { - SelectableAdapter.Mode.SINGLE - } - // SY --> - adapter.isLongPressDragEnabled = adapter.canDrag() - // SY <-- - - // EXH --> - initialLoadHandle = controller.loaderManager.openProgressBar() - // EXH <-- - - subscriptions += controller.searchRelay - .doOnNext { adapter.searchText = it } - .skip(1) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - // EXH --> - scope.launch { - val handle = controller.loaderManager.openProgressBar() - try { - // EXH <-- - adapter.performFilter(this) - // EXH --> - } finally { - controller.loaderManager.closeProgressBar(handle) - } - } - // EXH <-- - } - - subscriptions += controller.libraryMangaRelay - .subscribe { - // EXH --> - scope.launch { - try { - // EXH <-- - onNextLibraryManga(this, it) - // EXH --> - } finally { - controller.loaderManager.closeProgressBar(initialLoadHandle) - } - } - // EXH <-- - } - - subscriptions += controller.selectionRelay - .subscribe { onSelectionChanged(it) } - - subscriptions += controller.selectAllRelay - .filter { it == category.id } - .subscribe { - adapter.currentItems.forEach { item -> - controller.setSelection(item.manga.toDomainManga()!!, true) - } - controller.invalidateActionMode() - } - - subscriptions += controller.selectInverseRelay - .filter { it == category.id } - .subscribe { - adapter.currentItems.forEach { item -> - controller.toggleSelection(item.manga.toDomainManga()!!) - } - controller.invalidateActionMode() - } - } - - fun onRecycle() { - // SY --> - runBlocking { adapter.setItems(this, emptyList()) } - // SY <-- - adapter.clearSelection() - unsubscribe() - } - - fun onDestroy() { - unsubscribe() - scope.cancel() - // SY --> - controller.loaderManager.closeProgressBar(initialLoadHandle) - // SY <-- - } - - private fun unsubscribe() { - subscriptions.clear() - } - - /** - * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the - * adapter. - * - * @param event the event received. - */ - private suspend fun onNextLibraryManga(cScope: CoroutineScope, event: LibraryMangaEvent) { - // Get the manga list for this category. - // SY --> - adapter.isLongPressDragEnabled = adapter.canDrag() - var mangaForCategory = event.getMangaForCategory(category).orEmpty() - var mangaOrder = category.mangaOrder - if (preferences.categorizedDisplaySettings().get() && category.id != 0L) { - if (SortModeSetting.fromFlag(category.sortMode) == SortModeSetting.DRAG_AND_DROP) { - mangaForCategory = mangaForCategory.sortedBy { - mangaOrder.indexOf(it.manga.id) - } - } - } else if (preferences.librarySortingMode().get() == SortModeSetting.DRAG_AND_DROP) { - if (category.id == 0L) { - mangaOrder = preferences.defaultMangaOrder().get() - .split("/") - .mapNotNull { it.toLongOrNull() } - } - mangaForCategory = mangaForCategory.sortedBy { - mangaOrder.indexOf(it.manga.id) - } - } - // SY <-- - // Update the category with its manga. - // EXH --> - adapter.setItems(cScope, mangaForCategory) - // EXH <-- - - if (adapter.mode == SelectableAdapter.Mode.MULTI) { - controller.selectedMangas.forEach { manga -> - val position = adapter.indexOf(manga) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - (recycler.findViewHolderForItemId(manga.id) as? LibraryHolder<*>)?.toggleActivation() - } - } - } - } - - /** - * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection - * depending on the type of event received. - * - * @param event the selection event received. - */ - private fun onSelectionChanged(event: LibrarySelectionEvent) { - when (event) { - is LibrarySelectionEvent.Selected -> { - if (adapter.mode != SelectableAdapter.Mode.MULTI) { - adapter.mode = SelectableAdapter.Mode.MULTI - // SY --> - adapter.isLongPressDragEnabled = adapter.canDrag() - // SY <-- - } - findAndToggleSelection(event.manga) - } - is LibrarySelectionEvent.Unselected -> { - findAndToggleSelection(event.manga) - - with(adapter.indexOf(event.manga)) { - if (this != -1) lastClickPositionStack.remove(this) - } - - if (controller.selectedMangas.isEmpty()) { - adapter.mode = SelectableAdapter.Mode.SINGLE - // SY --> - adapter.isLongPressDragEnabled = adapter.canDrag() - // SY <-- - } - } - is LibrarySelectionEvent.Cleared -> { - adapter.mode = SelectableAdapter.Mode.SINGLE - adapter.clearSelection() - - lastClickPositionStack.clear() - lastClickPositionStack.push(-1) - // SY --> - adapter.isLongPressDragEnabled = adapter.canDrag() - // SY <-- - } - } - } - - /** - * Toggles the selection for the given manga and updates the view if needed. - * - * @param manga the manga to toggle. - */ - private fun findAndToggleSelection(manga: Manga) { - val position = adapter.indexOf(manga) - if (position != -1) { - adapter.toggleSelection(position) - (recycler.findViewHolderForItemId(manga.id) as? LibraryHolder<*>)?.toggleActivation() - } - } - - /** - * Called when a manga is clicked. - * - * @param position the position of the element clicked. - * @return true if the item should be selected, false otherwise. - */ - override fun onItemClick(view: View?, position: Int): Boolean { - // If the action mode is created and the position is valid, toggle the selection. - val item = adapter.getItem(position) ?: return false - return if (adapter.mode == SelectableAdapter.Mode.MULTI) { - if (adapter.isSelected(position)) { - lastClickPositionStack.remove(position) - } else { - lastClickPositionStack.push(position) - } - toggleSelection(position) - true - } else { - openManga(item.manga.toDomainManga()!!) - false - } - } - - /** - * Called when a manga is long clicked. - * - * @param position the position of the element clicked. - */ - override fun onItemLongClick(position: Int) { - controller.createActionModeIfNeeded() - val lastClickPosition = lastClickPositionStack.peek()!! - // SY --> - adapter.isLongPressDragEnabled = adapter.canDrag() - // SY <-- - when { - lastClickPosition == -1 -> setSelection(position) - lastClickPosition > position -> - for (i in position until lastClickPosition) - setSelection(i) - lastClickPosition < position -> - for (i in lastClickPosition + 1..position) - setSelection(i) - else -> setSelection(position) - } - if (lastClickPosition != position) { - lastClickPositionStack.remove(position) - lastClickPositionStack.push(position) - } - } - - // SY --> - private fun getGroupExtra() = when (controller.presenter.groupType) { - LibraryGroup.BY_DEFAULT -> null - LibraryGroup.BY_SOURCE, LibraryGroup.BY_STATUS, LibraryGroup.BY_TRACK_STATUS -> category.id.toString() - else -> null - } - - override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { - if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition) - return true - } - - override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { - val position = viewHolder?.bindingAdapterPosition ?: return - if (actionState == 2) { - onItemLongClick(position) - } - } - - private val updateCategory: UpdateCategory by injectLazy() - - override fun onItemMove(fromPosition: Int, toPosition: Int) { - if (fromPosition == toPosition) return - controller.invalidateActionMode() - val mangaIds = adapter.currentItems.mapNotNull { it.manga.id } - if (category.id == 0L) { - preferences.defaultMangaOrder().set(mangaIds.joinToString("/")) - } else { - scope.launch { - updateCategory.await(CategoryUpdate(category.id.toLong(), mangaOrder = mangaIds)) - } - } - if (preferences.categorizedDisplaySettings().get() && category.id != 0L) { - if (SortModeSetting.fromFlag(category.sortMode) != SortModeSetting.DRAG_AND_DROP) { - val dbCategory = category.toDbCategory() - dbCategory.sortMode = SortModeSetting.DRAG_AND_DROP.flag.toInt() - dbCategory.sortDirection = SortDirectionSetting.ASCENDING.flag.toInt() - scope.launch { - updateCategory.await( - CategoryUpdate( - id = category.id, - flags = dbCategory.flags.toLong(), - mangaOrder = mangaIds, - ), - ) - } - } - } else if (preferences.librarySortingMode().get() != SortModeSetting.DRAG_AND_DROP) { - preferences.librarySortingAscending().set(SortDirectionSetting.ASCENDING) - preferences.librarySortingMode().set(SortModeSetting.DRAG_AND_DROP) - } - } - // SY <-- - - /** - * Opens a manga. - * - * @param manga the manga to open. - */ - private fun openManga(manga: Manga) { - controller.openManga(manga) - } - - /** - * Tells the presenter to toggle the selection for the given position. - * - * @param position the position to toggle. - */ - private fun toggleSelection(position: Int) { - val item = adapter.getItem(position) ?: return - - controller.setSelection(item.manga.toDomainManga()!!, !adapter.isSelected(position)) - controller.invalidateActionMode() - } - - /** - * Tells the presenter to set the selection for the given position. - * - * @param position the position to toggle. - */ - private fun setSelection(position: Int) { - val item = adapter.getItem(position) ?: return - - controller.setSelection(item.manga.toDomainManga()!!, true) - controller.invalidateActionMode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt deleted file mode 100644 index b7b9eff3e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt +++ /dev/null @@ -1,92 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import coil.dispose -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.toDomainManga -import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding -import eu.kanade.tachiyomi.util.view.loadAutoPause -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.clicks - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_source_grid" are available in this class. - * - * @param binding the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. - */ -class LibraryComfortableGridHolder( - override val binding: SourceComfortableGridItemBinding, - adapter: FlexibleAdapter>, -) : LibraryHolder(binding.root, adapter) { - - // SY --> - var manga: Manga? = null - - init { - binding.playLayout.clicks() - .onEach { - playButtonClicked() - } - .launchIn((adapter as LibraryCategoryAdapter).controller.viewScope) - } - // SY <-- - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param item the manga item to bind. - */ - override fun onSetValues(item: LibraryItem) { - // SY --> - manga = item.manga - // SY <-- - // Update the title of the manga. - binding.title.text = item.manga.title - - // For rounded corners - binding.badges.leftBadges.clipToOutline = true - binding.badges.rightBadges.clipToOutline = true - - // Update the unread count and its visibility. - with(binding.badges.unreadText) { - isVisible = item.unreadCount > 0 - text = item.unreadCount.toString() - } - // Update the download count and its visibility. - with(binding.badges.downloadText) { - isVisible = item.downloadCount > 0 - text = item.downloadCount.toString() - } - // Update the source language and its visibility - with(binding.badges.languageText) { - isVisible = item.sourceLanguage.isNotEmpty() - text = item.sourceLanguage - } - // set local visibility if its local manga - binding.badges.localText.isVisible = item.isLocal - - // SY --> - binding.playLayout.isVisible = (item.manga.unreadCount > 0 && item.startReadingButton) - // SY <-- - - // Update the cover. - binding.thumbnail.dispose() - binding.thumbnail.loadAutoPause(item.manga) - } - - // SY --> - private fun playButtonClicked() { - if (adapter !is LibraryCategoryAdapter) return - adapter.controller.startReading(manga?.toDomainManga() ?: return, adapter) - } - // SY <-- -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt deleted file mode 100755 index 41bcd83e6..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt +++ /dev/null @@ -1,124 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import coil.dispose -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.toDomainManga -import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding -import eu.kanade.tachiyomi.util.view.loadAutoPause -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.clicks - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "source_compact_grid_item" are available in this class. - * - * @param binding the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param coverOnly true if title should be hidden a.k.a cover only mode. - * @constructor creates a new library holder. - */ -class LibraryCompactGridHolder( - override val binding: SourceCompactGridItemBinding, - adapter: FlexibleAdapter<*>, - private val coverOnly: Boolean, -) : LibraryHolder(binding.root, adapter) { - - // SY --> - var manga: Manga? = null - - init { - binding.playLayout.clicks() - .onEach { - playButtonClicked() - } - .launchIn((adapter as LibraryCategoryAdapter).controller.viewScope) - } - // SY <-- - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param item the manga item to bind. - */ - override fun onSetValues(item: LibraryItem) { - // SY --> - manga = item.manga - // SY <-- - // Update the title of the manga. - binding.title.text = item.manga.title - - // For rounded corners - binding.badges.leftBadges.clipToOutline = true - binding.badges.rightBadges.clipToOutline = true - - // Update the unread count and its visibility. - with(binding.badges.unreadText) { - isVisible = item.unreadCount > 0 - text = item.unreadCount.toString() - } - // Update the download count and its visibility. - with(binding.badges.downloadText) { - isVisible = item.downloadCount > 0 - text = item.downloadCount.toString() - } - // Update the source language and its visibility - with(binding.badges.languageText) { - isVisible = item.sourceLanguage.isNotEmpty() - text = item.sourceLanguage - } - // set local visibility if its local manga - binding.badges.localText.isVisible = item.isLocal - - // SY --> - binding.playLayout.updateLayoutParams { - when { - coverOnly -> { - topToBottom = -1 - topToTop = -1 - bottomToBottom = binding.thumbnail.id - } - item.sourceLanguage.isNotEmpty() -> { - topToBottom = binding.badges.root.id - topToTop = -1 - bottomToBottom = -1 - } - else -> { - topToBottom = -1 - topToTop = binding.thumbnail.id - bottomToBottom = -1 - } - } - } - binding.playLayout.isVisible = (item.manga.unreadCount > 0 && item.startReadingButton) - // SY <-- - - // Update the cover. - binding.thumbnail.dispose() - if (coverOnly) { - // Cover only mode: Hides title text unless thumbnail is unavailable - if (!item.manga.thumbnail_url.isNullOrEmpty()) { - binding.thumbnail.loadAutoPause(item.manga) - binding.title.isVisible = false - } else { - binding.title.text = item.manga.title - binding.title.isVisible = true - } - binding.thumbnail.foreground = null - } else { - binding.thumbnail.loadAutoPause(item.manga) - } - } - - // SY --> - private fun playButtonClicked() { - if (adapter !is LibraryCategoryAdapter) return - adapter.controller.startReading(manga?.toDomainManga() ?: return, adapter) - } - // SY <-- -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 565992576..f78ce6dd7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -17,18 +17,16 @@ import com.fredporciuncula.flow.preferences.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayout import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.davidea.flexibleadapter.SelectableAdapter import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.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.data.track.TrackManager 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 @@ -40,7 +38,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI -import eu.kanade.tachiyomi.util.preference.asImmediateFlow import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast @@ -97,42 +94,11 @@ class LibraryController( */ private var actionMode: ActionModeWithToolbar? = null - /** - * Currently selected mangas. - */ - val selectedMangas = mutableSetOf() - - /** - * Relay to notify the UI of selection updates. - */ - val selectionRelay: PublishRelay = PublishRelay.create() - - /** - * Relay to notify search query changes. - */ - val searchRelay: BehaviorRelay = BehaviorRelay.create() - /** * Relay to notify the library's viewpager for updates. */ val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() - /** - * Relay to notify the library's viewpager to select all manga - */ - val selectAllRelay: PublishRelay = PublishRelay.create() - - /** - * Relay to notify the library's viewpager to select the inverse - */ - val selectInverseRelay: PublishRelay = PublishRelay.create() - - /** - * Number of manga per row in grid mode. - */ - var mangaPerRow = 0 - private set - /** * Adapter of the view pager. */ @@ -210,7 +176,24 @@ class LibraryController( override fun onViewCreated(view: View) { super.onViewCreated(view) - adapter = LibraryAdapter(this) + adapter = LibraryAdapter( + controller = this, + presenter = presenter, + onClickManga = { + openManga(it.id!!) + }, + // SY --> + onOpenReader = { + startReading(it.toDomainManga()!!) + }, + // SY <-- + ) + + getColumnsPreferenceForCurrentOrientation() + .asFlow() + .onEach { presenter.columns = it } + .launchIn(viewScope) + binding.libraryPager.adapter = adapter binding.libraryPager.pageSelections() .drop(1) @@ -221,13 +204,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() } @@ -261,6 +238,14 @@ class LibraryController( .launchIn(viewScope) } + private fun getColumnsPreferenceForCurrentOrientation(): Preference { + 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) { @@ -271,7 +256,6 @@ class LibraryController( override fun onDestroyView(view: View) { destroyActionModeIfNeeded() - adapter?.onDestroy() adapter = null settingsSheet?.sheetScope?.cancel() settingsSheet = null @@ -355,6 +339,12 @@ class LibraryController( } } + presenter.loadedManga.clear() + mangaMap.forEach { + presenter.loadedManga[it.key] = it.value + } + presenter.loadedMangaFlow.value = presenter.loadedManga + // Send the manga map to child fragments after the adapter is updated. libraryMangaRelay.call(LibraryMangaEvent(mangaMap)) @@ -362,19 +352,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 { - return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) { - preferences.portraitColumns() - } else { - preferences.landscapeColumns() - } - } - private fun onFilterChanged() { presenter.requestFilterUpdate() activity?.invalidateOptionsMenu() @@ -462,7 +439,6 @@ class LibraryController( } private fun performSearch() { - searchRelay.call(presenter.query) if (presenter.query.isNotEmpty()) { binding.btnGlobalSearch.isVisible = true binding.btnGlobalSearch.text = @@ -533,7 +509,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() @@ -544,17 +520,17 @@ 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 } } } // SY --> - toolbar.findToolbarItem(R.id.action_clean)?.isVisible = selectedMangas.any { + toolbar.findToolbarItem(R.id.action_clean)?.isVisible = presenter.selection.any { it.isEhBasedManga() || it.source in nHentaiSourceIds || it.source == PERV_EDEN_EN_SOURCE_ID || it.source == PERV_EDEN_IT_SOURCE_ID } - toolbar.findToolbarItem(R.id.action_push_to_mdlist)?.isVisible = trackManager.mdList.isLogged && selectedMangas.any { + toolbar.findToolbarItem(R.id.action_push_to_mdlist)?.isVisible = trackManager.mdList.isLogged && presenter.selection.any { it.source in mangaDexSourceIds } // SY <-- @@ -572,7 +548,7 @@ class LibraryController( // SY --> R.id.action_migrate -> { val skipPre = preferences.skipPreMigration().get() - val selectedMangaIds = selectedMangas.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id } + val selectedMangaIds = presenter.selection.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id } clearSelection() if (selectedMangaIds.isNotEmpty()) { PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds) @@ -590,50 +566,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)) } /** @@ -641,8 +585,7 @@ class LibraryController( * invalidate the action mode to revert the top toolbar */ fun clearSelection() { - selectedMangas.clear() - selectionRelay.call(LibrarySelectionEvent.Cleared) + presenter.clearSelection() invalidateActionMode() } @@ -652,15 +595,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 @@ -669,31 +612,32 @@ 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) } // SY --> private fun cleanTitles() { - val mangas = selectedMangas.filter { + val mangas = presenter.selection.filter { it.isEhBasedManga() || it.source in nHentaiSourceIds || it.source == PERV_EDEN_EN_SOURCE_ID || @@ -704,7 +648,7 @@ class LibraryController( } private fun pushToMdList() { - val mangas = selectedMangas.filter { + val mangas = presenter.selection.filter { it.source in mangaDexSourceIds } presenter.syncMangaToDex(mangas) @@ -746,21 +690,18 @@ class LibraryController( // SY <-- 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() } } @@ -816,7 +757,7 @@ class LibraryController( ?.setMessage(activity!!.getString(R.string.favorites_sync_bad_library_state, status.message)) ?.setCancelable(false) ?.setPositiveButton(R.string.show_gallery) { _, _ -> - openManga(status.manga) + openManga(status.manga.id) presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!) } ?.setNegativeButton(android.R.string.ok) { _, _ -> @@ -877,11 +818,7 @@ class LibraryController( } } - fun startReading(manga: Manga, adapter: LibraryCategoryAdapter) { - if (adapter.mode == SelectableAdapter.Mode.MULTI) { - toggleSelection(manga) - return - } + private fun startReading(manga: Manga) { val activity = activity ?: return val chapter = presenter.getFirstUnread(manga) ?: return val intent = ReaderActivity.newIntent(activity, manga.id, chapter.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt deleted file mode 100755 index 6a93ec8e5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import androidx.viewbinding.ViewBinding -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.viewholders.FlexibleViewHolder - -/** - * Generic class used to hold the displayed data of a manga in the library. - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to the single tap and long tap events. - */ - -abstract class LibraryHolder( - view: View, - val adapter: FlexibleAdapter<*>, -) : FlexibleViewHolder(view, adapter) { - - abstract val binding: VB - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param item the manga item to bind. - */ - abstract fun onSetValues(item: LibraryItem) - - // SY --> - override fun onLongClick(view: View?): Boolean { - return if (adapter.isLongPressDragEnabled) { - super.onLongClick(view) - false - } else super.onLongClick(view) - } - // SY <-- -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 62008d35a..2d612c143 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -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, - private val defaultLibraryDisplayMode: Preference, -) : - AbstractFlexibleItem>(), IFilterable { +) { private val sourceManager: SourceManager = Injekt.get() @@ -35,63 +21,13 @@ class LibraryItem( var startReadingButton = false // SY <-- - private fun getDisplayMode(): DisplayModeSetting { - return if (shouldSetFromCategory.get() && manga.category != 0) { - DisplayModeSetting.fromFlag(displayMode) - } else { - defaultLibraryDisplayMode.get() - } - } - - override fun getLayoutRes(): Int { - return when (getDisplayMode()) { - DisplayModeSetting.COMPACT_GRID, DisplayModeSetting.COVER_ONLY_GRID -> R.layout.source_compact_grid_item - DisplayModeSetting.COMFORTABLE_GRID -> R.layout.source_comfortable_grid_item - DisplayModeSetting.LIST -> R.layout.source_list_item - } - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): 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>, - holder: LibraryHolder<*>, - position: Int, - payloads: List?, - ) { - holder.onSetValues(this) - } - - // SY --> - /** - * Returns true if this item is draggable. - */ - override fun isDraggable(): Boolean { - return true - } - /** * 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) || diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt deleted file mode 100755 index 7d4b2c277..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ /dev/null @@ -1,67 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import androidx.core.view.isVisible -import coil.dispose -import coil.load -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.databinding.SourceListItemBinding - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_library_list" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. - */ -class LibraryListHolder( - private val view: View, - adapter: FlexibleAdapter<*>, -) : LibraryHolder(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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 0d1954833..4762c7dff 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -1,6 +1,15 @@ package eu.kanade.tachiyomi.ui.library import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.util.fastAny import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.core.util.asObservable import eu.kanade.data.DatabaseHandler @@ -13,8 +22,11 @@ import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.UpdateChapter import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata import eu.kanade.domain.manga.interactor.GetLibraryManga import eu.kanade.domain.manga.interactor.GetMergedMangaById +import eu.kanade.domain.manga.interactor.GetSearchTags +import eu.kanade.domain.manga.interactor.GetSearchTitles import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.MangaUpdate @@ -22,6 +34,7 @@ import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.tachiyomi.R 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.library.CustomMangaManager @@ -33,20 +46,37 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.all.MergedSource 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 import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import exh.favorites.FavoritesSyncHelper import exh.md.utils.FollowStatus import exh.md.utils.MdUtil +import exh.metadata.sql.models.SearchTag +import exh.metadata.sql.models.SearchTitle +import exh.search.Namespace +import exh.search.QueryComponent +import exh.search.SearchEngine +import exh.search.Text import exh.source.MERGED_SOURCE_ID import exh.source.isEhBasedManga +import exh.source.isMetadataSource +import exh.util.cancellable import exh.util.isLewd import exh.util.nullIfBlank +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import rx.Observable import rx.Subscription @@ -88,9 +118,13 @@ class LibraryPresenter( private val downloadManager: DownloadManager = Injekt.get(), private val trackManager: TrackManager = Injekt.get(), // SY --> + private val searchEngine: SearchEngine = SearchEngine(), private val customMangaManager: CustomMangaManager = Injekt.get(), private val getMergedMangaById: GetMergedMangaById = Injekt.get(), private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(), + private val getIdsOfFavoriteMangaWithMetadata: GetIdsOfFavoriteMangaWithMetadata = Injekt.get(), + private val getSearchTags: GetSearchTags = Injekt.get(), + private val getSearchTitles: GetSearchTitles = Injekt.get(), // SY <-- ) : BasePresenter() { @@ -99,9 +133,24 @@ class LibraryPresenter( /** * Categories of the library. */ - var categories: List = emptyList() + var categories: List = mutableStateListOf() private set + var loadedManga = mutableStateMapOf>() + private set + + val loadedMangaFlow = MutableStateFlow(loadedManga) + + var searchQuery by mutableStateOf(query) + + val selection: MutableList = 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. */ @@ -132,6 +181,10 @@ class LibraryPresenter( private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } + private val services = trackManager.services.associate { service -> + service.id to context.getString(service.nameRes()) + } + /** * Relay used to apply the UI update to the last emission of the library. */ @@ -145,6 +198,14 @@ class LibraryPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) + preferences.libraryDisplayMode() + .asFlow() + .drop(1) + .onEach { + currentDisplayMode = it + } + .launchIn(presenterScope) + subscribeLibrary() } @@ -567,11 +628,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() } } } @@ -711,7 +768,7 @@ class LibraryPresenter( } // SY --> - fun cleanTitles(mangas: List) { + fun cleanTitles(mangas: List) { mangas.forEach { manga -> val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let { if (it.contains("|")) { @@ -724,18 +781,18 @@ class LibraryPresenter( val mangaJson = CustomMangaManager.MangaJson( id = manga.id, title = editedTitle.nullIfBlank(), - author = manga.author.takeUnless { it == manga.ogAuthor }, - artist = manga.artist.takeUnless { it == manga.ogArtist }, - description = manga.description.takeUnless { it == manga.ogDescription }, - genre = manga.genre.takeUnless { it == manga.ogGenre }, - status = manga.status.takeUnless { it == manga.ogStatus }?.toLong(), + author = manga.author.takeUnless { it == manga.originalAuthor }, + artist = manga.artist.takeUnless { it == manga.originalArtist }, + description = manga.description.takeUnless { it == manga.originalDescription }, + genre = manga.getGenres().takeUnless { it == manga.getOriginalGenres() }, + status = manga.status.takeUnless { it == manga.originalStatus }?.toLong(), ) customMangaManager.saveMangaInfo(mangaJson) } } - fun syncMangaToDex(mangaList: List) { + fun syncMangaToDex(mangaList: List) { launchIO { MdUtil.getEnabledMangaDex(preferences, sourceManager)?.let { mdex -> mangaList.forEach { @@ -821,6 +878,173 @@ class LibraryPresenter( } } + // SY --> + @Composable + fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State> { + val unfiltered = loadedManga[categoryId] ?: emptyList() + + return produceState(initialValue = unfiltered, searchQuery) { + val query = searchQuery + value = withIOContext { + if (unfiltered.isNotEmpty() && query.isNotBlank()) { + // Prepare filter object + val parsedQuery = searchEngine.parseQuery(query) + val mangaWithMetaIds = getIdsOfFavoriteMangaWithMetadata.await() + unfiltered.asFlow().cancellable().filter { item -> + if (isMetadataSource(item.manga.source)) { + val mangaId = item.manga.id ?: -1 + if (mangaWithMetaIds.binarySearch(mangaId) < 0) { + // No meta? Filter using title + filterManga(parsedQuery, item.manga) + } else { + val tags = getSearchTags.await(mangaId) + val titles = getSearchTitles.await(mangaId) + filterManga(parsedQuery, item.manga, false, tags, titles) + } + } else { + filterManga(parsedQuery, item.manga) + } + }.toList() + } else { + unfiltered + } + } + } + } + + private suspend fun filterManga( + queries: List, + manga: LibraryManga, + checkGenre: Boolean = true, + searchTags: List? = null, + searchTitles: List? = null, + ): Boolean { + val mappedQueries = queries.groupBy { it.excluded } + val tracks = if (loggedServices.isNotEmpty()) getTracks.await(manga.id!!).toList() else null + val source = sourceManager.get(manga.source) + val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList() + val hasNormalQuery = mappedQueries[false]?.all { queryComponent -> + when (queryComponent) { + is Text -> { + val query = queryComponent.asQuery() + manga.title.contains(query, true) || + (manga.author?.contains(query, true) == true) || + (manga.artist?.contains(query, true) == true) || + (manga.description?.contains(query, true) == true) || + (source?.name?.contains(query, true) == true) || + (loggedServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) || + (genre.any { it.contains(query, true) }) || + (searchTags.orEmpty().any { it.name.contains(query, true) }) || + (searchTitles.orEmpty().any { it.title.contains(query, true) }) + } + is Namespace -> { + searchTags != null && searchTags.any { + val tag = queryComponent.tag + (it.namespace != null && it.namespace.contains(queryComponent.namespace, true) && tag != null && it.name.contains(tag.asQuery(), true)) || + (tag == null && it.namespace != null && it.namespace.contains(queryComponent.namespace, true)) + } + } + else -> true + } + } + val doesNotHaveExcludedQuery = mappedQueries[true]?.all { queryComponent -> + when (queryComponent) { + is Text -> { + val query = queryComponent.asQuery() + query.isBlank() || ( + (!manga.title.contains(query, true)) && + (!manga.author.orEmpty().contains(query, true)) && + (!manga.artist.orEmpty().contains(query, true)) && + (!manga.description.orEmpty().contains(query, true)) && + (!source?.name.orEmpty().contains(query, true)) && + (loggedServices.isEmpty() || loggedServices.isNotEmpty() && tracks == null || tracks != null && !filterTracks(query, tracks)) && + (genre.none { it.contains(query, true) }) && + (searchTags.orEmpty().none { it.name.contains(query, true) }) && + (searchTitles.orEmpty().none { it.title.contains(query, true) }) + ) + } + is Namespace -> { + val searchedTag = queryComponent.tag?.asQuery() + searchTags == null || searchTags.all { mangaTag -> + if (searchedTag == null || searchedTag.isBlank()) { + mangaTag.namespace == null || !mangaTag.namespace.contains(queryComponent.namespace, true) + } else if (mangaTag.namespace == null) { + true + } else { + !(mangaTag.name.contains(searchedTag, true) && mangaTag.namespace.contains(queryComponent.namespace, true)) + } + } + } + else -> true + } + } + + return (hasNormalQuery != null && doesNotHaveExcludedQuery != null && hasNormalQuery && doesNotHaveExcludedQuery) || + (hasNormalQuery != null && doesNotHaveExcludedQuery == null && hasNormalQuery) || + (hasNormalQuery == null && doesNotHaveExcludedQuery != null && doesNotHaveExcludedQuery) + } + + private fun filterTracks(constraint: String, tracks: List): Boolean { + return tracks.any { + val trackService = trackManager.getService(it.syncId) + if (trackService != null) { + val status = trackService.getStatus(it.status.toInt()) + val name = services[it.syncId] + return@any status.contains(constraint, true) || name?.contains(constraint, true) == true + } + return@any false + } + } + // SY <-- + + @Composable + fun getDisplayMode(index: Int): DisplayModeSetting { + val category = categories[index] + return remember { + if (isPerCategory.not() || category.id == 0L) { + currentDisplayMode + } else { + DisplayModeSetting.fromFlag(category.displayMode) + } + } + } + + fun hasSelection(): Boolean { + return selection.isNotEmpty() + } + + fun clearSelection() { + selection.clear() + } + + fun toggleSelection(manga: LibraryManga) { + if (selection.fastAny { it.id == manga.id }) { + selection.remove(manga) + } else { + selection.add(manga) + } + view?.invalidateActionMode() + view?.createActionModeIfNeeded() + } + + fun selectAll(index: Int) { + val category = categories[index] + val items = loadedManga[category.id] ?: emptyList() + selection.addAll(items.filterNot { it.manga in selection }.map { it.manga }) + view?.createActionModeIfNeeded() + view?.invalidateActionMode() + } + + fun invertSelection(index: Int) { + val category = categories[index] + val items = (loadedManga[category.id] ?: emptyList()).map { it.manga } + val invert = items.filterNot { it in selection } + selection.removeAll(items) + selection.addAll(invert) + view?.createActionModeIfNeeded() + view?.invalidateActionMode() + } + // SY --> /** Returns first unread chapter of a manga */ fun getFirstUnread(manga: Manga): Chapter? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt deleted file mode 100755 index 7b93e2502..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/res/layout/library_category.xml b/app/src/main/res/layout/library_category.xml deleted file mode 100755 index ff3f50ef7..000000000 --- a/app/src/main/res/layout/library_category.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/library_grid_recycler.xml b/app/src/main/res/layout/library_grid_recycler.xml deleted file mode 100755 index e80b60499..000000000 --- a/app/src/main/res/layout/library_grid_recycler.xml +++ /dev/null @@ -1,14 +0,0 @@ - - diff --git a/app/src/main/res/layout/library_list_recycler.xml b/app/src/main/res/layout/library_list_recycler.xml deleted file mode 100755 index 2604c02bd..000000000 --- a/app/src/main/res/layout/library_list_recycler.xml +++ /dev/null @@ -1,9 +0,0 @@ - -