Просмотр исходного кода

Use Compose for Library list and grid (#7520)

Andreas 2 лет назад
Родитель
Сommit
905c96922b
23 измененных файлов с 855 добавлено и 896 удалено
  1. 48 0
      app/src/main/java/eu/kanade/presentation/library/components/Badge.kt
  2. 30 0
      app/src/main/java/eu/kanade/presentation/library/components/LazyLibraryGrid.kt
  3. 82 0
      app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt
  4. 104 0
      app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt
  5. 68 0
      app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt
  6. 74 0
      app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt
  7. 46 0
      app/src/main/java/eu/kanade/presentation/library/components/LibraryGridItemSelectable.kt
  8. 121 0
      app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt
  9. 12 0
      app/src/main/java/eu/kanade/presentation/util/Modifier.kt
  10. 113 65
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt
  11. 0 44
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
  12. 0 328
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
  13. 0 61
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt
  14. 0 72
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt
  15. 49 112
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
  16. 0 29
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
  17. 2 58
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
  18. 0 67
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
  19. 106 6
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  20. 0 9
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt
  21. 0 22
      app/src/main/res/layout/library_category.xml
  22. 0 14
      app/src/main/res/layout/library_grid_recycler.xml
  23. 0 9
      app/src/main/res/layout/library_list_recycler.xml

+ 48 - 0
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,
+        )
+    }
+}

+ 30 - 0
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,
+    )
+}

+ 82 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt

@@ -0,0 +1,82 @@
+package eu.kanade.presentation.library.components
+
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import eu.kanade.domain.manga.model.MangaCover
+import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.ui.library.LibraryItem
+
+@Composable
+fun LibraryComfortableGrid(
+    items: List<LibraryItem>,
+    columns: Int,
+    selection: List<LibraryManga>,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    LazyLibraryGrid(
+        columns = columns,
+    ) {
+        items(
+            items = items,
+            key = {
+                it.manga.id!!
+            },
+        ) { libraryItem ->
+            LibraryComfortableGridItem(
+                libraryItem,
+                libraryItem.manga in selection,
+                onClick,
+                onLongClick,
+            )
+        }
+    }
+}
+
+@Composable
+fun LibraryComfortableGridItem(
+    item: LibraryItem,
+    isSelected: Boolean,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    val manga = item.manga
+    LibraryGridItemSelectable(isSelected = isSelected) {
+        Column(
+            modifier = Modifier
+                .combinedClickable(
+                    onClick = {
+                        onClick(manga)
+                    },
+                    onLongClick = {
+                        onLongClick(manga)
+                    },
+                ),
+        ) {
+            LibraryGridCover(
+                mangaCover = MangaCover(
+                    manga.id!!,
+                    manga.source,
+                    manga.favorite,
+                    manga.thumbnail_url,
+                    manga.cover_last_modified,
+                ),
+                downloadCount = item.downloadCount,
+                unreadCount = item.unreadCount,
+                isLocal = item.isLocal,
+                language = item.sourceLanguage,
+            )
+            Text(
+                text = manga.title,
+                maxLines = 2,
+                style = LocalTextStyle.current.copy(fontWeight = FontWeight.SemiBold),
+            )
+        }
+    }
+}

+ 104 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt

@@ -0,0 +1,104 @@
+package eu.kanade.presentation.library.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.ui.library.LibraryItem
+
+@Composable
+fun LibraryCompactGrid(
+    items: List<LibraryItem>,
+    columns: Int,
+    selection: List<LibraryManga>,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    LazyLibraryGrid(
+        columns = columns,
+    ) {
+        items(
+            items = items,
+            key = {
+                it.manga.id!!
+            },
+        ) { libraryItem ->
+            LibraryCompactGridItem(
+                item = libraryItem,
+                isSelected = libraryItem.manga in selection,
+                onClick = onClick,
+                onLongClick = onLongClick,
+            )
+        }
+    }
+}
+
+@Composable
+fun LibraryCompactGridItem(
+    item: LibraryItem,
+    isSelected: Boolean,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    val manga = item.manga
+    LibraryGridCover(
+        modifier = Modifier
+            .selectedOutline(isSelected)
+            .combinedClickable(
+                onClick = {
+                    onClick(manga)
+                },
+                onLongClick = {
+                    onLongClick(manga)
+                },
+            ),
+        mangaCover = eu.kanade.domain.manga.model.MangaCover(
+            manga.id!!,
+            manga.source,
+            manga.favorite,
+            manga.thumbnail_url,
+            manga.cover_last_modified,
+        ),
+        downloadCount = item.downloadCount,
+        unreadCount = item.unreadCount,
+        isLocal = item.isLocal,
+        language = item.sourceLanguage,
+    ) {
+        Box(
+            modifier = Modifier
+                .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
+                .background(
+                    Brush.verticalGradient(
+                        0f to Color.Transparent,
+                        1f to Color(0xAA000000),
+                    ),
+                )
+                .fillMaxHeight(0.33f)
+                .fillMaxWidth()
+                .align(Alignment.BottomCenter),
+        )
+        Text(
+            text = manga.title,
+            modifier = Modifier
+                .padding(8.dp)
+                .align(Alignment.BottomStart),
+            maxLines = 2,
+            style = LocalTextStyle.current.copy(color = Color.White, fontWeight = FontWeight.SemiBold),
+        )
+    }
+}

+ 68 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt

@@ -0,0 +1,68 @@
+package eu.kanade.presentation.library.components
+
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.ui.library.LibraryItem
+
+@Composable
+fun LibraryCoverOnlyGrid(
+    items: List<LibraryItem>,
+    columns: Int,
+    selection: List<LibraryManga>,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    LazyLibraryGrid(
+        columns = columns,
+    ) {
+        items(
+            items = items,
+            key = {
+                it.manga.id!!
+            },
+        ) { libraryItem ->
+            LibraryCoverOnlyGridItem(
+                item = libraryItem,
+                isSelected = libraryItem.manga in selection,
+                onClick = onClick,
+                onLongClick = onLongClick,
+            )
+        }
+    }
+}
+
+@Composable
+fun LibraryCoverOnlyGridItem(
+    item: LibraryItem,
+    isSelected: Boolean,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    val manga = item.manga
+    LibraryGridCover(
+        modifier = Modifier
+            .selectedOutline(isSelected)
+            .combinedClickable(
+                onClick = {
+                    onClick(manga)
+                },
+                onLongClick = {
+                    onLongClick(manga)
+                },
+            ),
+        mangaCover = eu.kanade.domain.manga.model.MangaCover(
+            manga.id!!,
+            manga.source,
+            manga.favorite,
+            manga.thumbnail_url,
+            manga.cover_last_modified,
+        ),
+        downloadCount = item.downloadCount,
+        unreadCount = item.unreadCount,
+        isLocal = item.isLocal,
+        language = item.sourceLanguage,
+    )
+}

+ 74 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt

@@ -0,0 +1,74 @@
+package eu.kanade.presentation.library.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import eu.kanade.presentation.components.MangaCover
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun LibraryGridCover(
+    modifier: Modifier = Modifier,
+    mangaCover: eu.kanade.domain.manga.model.MangaCover,
+    downloadCount: Int,
+    unreadCount: Int,
+    isLocal: Boolean,
+    language: String,
+    content: @Composable BoxScope.() -> Unit = {},
+) {
+    Box(
+        modifier = modifier
+            .fillMaxWidth()
+            .aspectRatio(MangaCover.Book.ratio),
+    ) {
+        MangaCover.Book(
+            modifier = Modifier.fillMaxWidth(),
+            data = mangaCover,
+        )
+        content()
+        BadgeGroup(
+            modifier = Modifier
+                .padding(4.dp)
+                .align(Alignment.TopStart),
+        ) {
+            if (downloadCount > 0) {
+                Badge(
+                    text = "$downloadCount",
+                    color = MaterialTheme.colorScheme.tertiary,
+                    textColor = MaterialTheme.colorScheme.onTertiary,
+                )
+            }
+            if (unreadCount > 0) {
+                Badge(text = "$unreadCount")
+            }
+        }
+        BadgeGroup(
+            modifier = Modifier
+                .padding(4.dp)
+                .align(Alignment.TopEnd),
+        ) {
+            if (isLocal) {
+                Badge(
+                    text = stringResource(id = R.string.local_source_badge),
+                    color = MaterialTheme.colorScheme.tertiary,
+                    textColor = MaterialTheme.colorScheme.onTertiary,
+                )
+            }
+            if (isLocal.not() && language.isNotEmpty()) {
+                Badge(
+                    text = language,
+                    color = MaterialTheme.colorScheme.tertiary,
+                    textColor = MaterialTheme.colorScheme.onTertiary,
+                )
+            }
+        }
+    }
+}

+ 46 - 0
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()
+        }
+    }
+}

+ 121 - 0
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<LibraryItem>,
+    columns: Int,
+    selection: List<LibraryManga>,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    LazyColumn(
+        contentPadding = WindowInsets.navigationBars.asPaddingValues(),
+    ) {
+        items(
+            items = items,
+            key = {
+                it.manga.id!!
+            },
+        ) { libraryItem ->
+            LibraryListItem(
+                item = libraryItem,
+                isSelected = libraryItem.manga in selection,
+                onClick = onClick,
+                onLongClick = onLongClick,
+            )
+        }
+    }
+}
+
+@Composable
+fun LibraryListItem(
+    item: LibraryItem,
+    isSelected: Boolean,
+    onClick: (LibraryManga) -> Unit,
+    onLongClick: (LibraryManga) -> Unit,
+) {
+    val manga = item.manga
+    Row(
+        modifier = Modifier
+            .selectedBackground(isSelected)
+            .height(56.dp)
+            .combinedClickable(
+                onClick = { onClick(manga) },
+                onLongClick = { onLongClick(manga) },
+            )
+            .padding(horizontal = horizontalPadding),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        eu.kanade.presentation.components.MangaCover.Square(
+            modifier = Modifier
+                .padding(vertical = verticalPadding)
+                .fillMaxHeight(),
+            data = MangaCover(
+                manga.id!!,
+                manga.source,
+                manga.favorite,
+                manga.thumbnail_url,
+                manga.cover_last_modified,
+            ),
+        )
+        Text(
+            text = manga.title,
+            modifier = Modifier
+                .padding(horizontal = horizontalPadding)
+                .weight(1f),
+            maxLines = 2,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+        BadgeGroup {
+            if (item.downloadCount > 0) {
+                Badge(
+                    text = "${item.downloadCount}",
+                    color = MaterialTheme.colorScheme.tertiary,
+                    textColor = MaterialTheme.colorScheme.onTertiary,
+                )
+            }
+            if (item.unreadCount > 0) {
+                Badge(text = "${item.unreadCount}")
+            }
+            if (item.isLocal) {
+                Badge(
+                    text = stringResource(id = R.string.local_source_badge),
+                    color = MaterialTheme.colorScheme.tertiary,
+                    textColor = MaterialTheme.colorScheme.onTertiary,
+                )
+            }
+            if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) {
+                Badge(
+                    text = item.sourceLanguage,
+                    color = MaterialTheme.colorScheme.tertiary,
+                    textColor = MaterialTheme.colorScheme.onTertiary,
+                )
+            }
+        }
+    }
+}

+ 12 - 0
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(

+ 113 - 65
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,15 @@ import uy.kohesive.injekt.api.get
  */
 class LibraryAdapter(
     private val controller: LibraryController,
+    private val presenter: LibraryPresenter,
+    private val onClickManga: (LibraryManga) -> Unit,
     private val preferences: PreferencesHelper = Injekt.get(),
 ) : RecyclerViewPagerAdapter() {
 
     /**
      * The categories to bind in the adapter.
      */
-    var categories: List<Category> = emptyList()
+    var categories: List<Category> = mutableStateListOf()
         private set
 
     /**
@@ -38,19 +60,6 @@ class LibraryAdapter(
 
     private var boundViews = arrayListOf<View>()
 
-    private val isPerCategory by lazy { preferences.categorizedDisplaySettings().get() }
-    private var currentDisplayMode = preferences.libraryDisplayMode().get()
-
-    init {
-        preferences.libraryDisplayMode()
-            .asFlow()
-            .drop(1)
-            .onEach {
-                currentDisplayMode = it
-            }
-            .launchIn(controller.viewScope)
-    }
-
     /**
      * Pair of category and size of category
      */
@@ -80,10 +89,8 @@ class LibraryAdapter(
      * @return a new view.
      */
     override fun inflateView(container: ViewGroup, viewType: Int): View {
-        val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
-        val view: LibraryCategoryView = binding.root
-        view.onCreate(controller, binding, viewType)
-        return view
+        val binding = ComposeControllerBinding.inflate(LayoutInflater.from(container.context), container, false)
+        return binding.root
     }
 
     /**
@@ -93,7 +100,89 @@ class LibraryAdapter(
      * @param position the position in the adapter.
      */
     override fun bindView(view: View, position: Int) {
-        (view as LibraryCategoryView).onBind(categories[position])
+        (view as ComposeView).apply {
+            consumeWindowInsets = false
+            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+            setContent {
+                TachiyomiTheme {
+                    CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
+                        val nestedScrollInterop = rememberNestedScrollInteropConnection()
+
+                        val category = presenter.categories[position]
+                        val displayMode = presenter.getDisplayMode(index = position)
+                        val mangaList by presenter.getMangaForCategory(categoryId = category.id)
+
+                        val onClickManga = { manga: LibraryManga ->
+                            if (presenter.hasSelection().not()) {
+                                onClickManga(manga)
+                            } else {
+                                presenter.toggleSelection(manga)
+                            }
+                        }
+                        val onLongClickManga = { manga: LibraryManga ->
+                            presenter.toggleSelection(manga)
+                        }
+
+                        SwipeRefresh(
+                            modifier = Modifier.nestedScroll(nestedScrollInterop),
+                            state = rememberSwipeRefreshState(isRefreshing = false),
+                            onRefresh = {
+                                if (LibraryUpdateService.start(context, category)) {
+                                    context.toast(R.string.updating_category)
+                                }
+                            },
+                            indicator = { s, trigger ->
+                                SwipeRefreshIndicator(
+                                    state = s,
+                                    refreshTriggerDistance = trigger,
+                                )
+                            },
+                        ) {
+                            when (displayMode) {
+                                DisplayModeSetting.LIST -> {
+                                    LibraryList(
+                                        items = mangaList,
+                                        columns = presenter.columns,
+                                        selection = presenter.selection,
+                                        onClick = onClickManga,
+                                        onLongClick = {
+                                            presenter.toggleSelection(it)
+                                        },
+                                    )
+                                }
+                                DisplayModeSetting.COMPACT_GRID -> {
+                                    LibraryCompactGrid(
+                                        items = mangaList,
+                                        columns = presenter.columns,
+                                        selection = presenter.selection,
+                                        onClick = onClickManga,
+                                        onLongClick = onLongClickManga,
+                                    )
+                                }
+                                DisplayModeSetting.COMFORTABLE_GRID -> {
+                                    LibraryComfortableGrid(
+                                        items = mangaList,
+                                        columns = presenter.columns,
+                                        selection = presenter.selection,
+                                        onClick = onClickManga,
+                                        onLongClick = onLongClickManga,
+                                    )
+                                }
+                                DisplayModeSetting.COVER_ONLY_GRID -> {
+                                    LibraryCoverOnlyGrid(
+                                        items = mangaList,
+                                        columns = presenter.columns,
+                                        selection = presenter.selection,
+                                        onClick = onClickManga,
+                                        onLongClick = onLongClickManga,
+                                    )
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
         boundViews.add(view)
     }
 
@@ -104,7 +193,6 @@ class LibraryAdapter(
      * @param position the position in the adapter.
      */
     override fun recycleView(view: View, position: Int) {
-        (view as LibraryCategoryView).onRecycle()
         boundViews.remove(view)
     }
 
@@ -131,45 +219,5 @@ class LibraryAdapter(
         }
     }
 
-    /**
-     * Returns the position of the view.
-     */
-    override fun getItemPosition(obj: Any): Int {
-        val view = obj as? LibraryCategoryView ?: return POSITION_NONE
-        val index = categories.indexOfFirst { it.id == view.category.id }
-        return if (index == -1) POSITION_NONE else index
-    }
-
-    /**
-     * Called when the view of this adapter is being destroyed.
-     */
-    fun onDestroy() {
-        for (view in boundViews) {
-            if (view is LibraryCategoryView) {
-                view.onDestroy()
-            }
-        }
-    }
-
-    override fun getViewType(position: Int): Int {
-        val category = categories.getOrNull(position)
-        return if (isPerCategory && category?.id != 0L) {
-            if (DisplayModeSetting.fromFlag(category?.displayMode) == DisplayModeSetting.LIST) {
-                LIST_DISPLAY_MODE
-            } else {
-                GRID_DISPLAY_MODE
-            }
-        } else {
-            if (currentDisplayMode == DisplayModeSetting.LIST) {
-                LIST_DISPLAY_MODE
-            } else {
-                GRID_DISPLAY_MODE
-            }
-        }
-    }
-
-    companion object {
-        const val LIST_DISPLAY_MODE = 1
-        const val GRID_DISPLAY_MODE = 2
-    }
+    override fun getViewType(position: Int): Int = -1
 }

+ 0 - 44
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt

@@ -1,44 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.domain.manga.model.Manga
-
-/**
- * Adapter storing a list of manga in a certain category.
- *
- * @param view the fragment containing this adapter.
- */
-class LibraryCategoryAdapter(view: LibraryCategoryView) :
-    FlexibleAdapter<LibraryItem>(null, view, true) {
-
-    /**
-     * The list of manga in this category.
-     */
-    private var mangas: List<LibraryItem> = emptyList()
-
-    /**
-     * Sets a list of manga in the adapter.
-     *
-     * @param list the list to set.
-     */
-    fun setItems(list: List<LibraryItem>) {
-        // A copy of manga always unfiltered.
-        mangas = list.toList()
-
-        performFilter()
-    }
-
-    /**
-     * Returns the position in the adapter for the given manga.
-     *
-     * @param manga the manga to find.
-     */
-    fun indexOf(manga: Manga): Int {
-        return currentItems.indexOfFirst { it.manga.id == manga.id }
-    }
-
-    fun performFilter() {
-        val s = getFilter(String::class.java) ?: ""
-        updateDataSet(mangas.filter { it.filter(s) })
-    }
-}

+ 0 - 328
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt

@@ -1,328 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import android.widget.FrameLayout
-import androidx.recyclerview.widget.LinearLayoutManager
-import dev.chrisbanes.insetter.applyInsetter
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.SelectableAdapter
-import eu.kanade.domain.category.model.Category
-import eu.kanade.domain.manga.model.Manga
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.toDomainManga
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.util.lang.plusAssign
-import eu.kanade.tachiyomi.util.system.toast
-import eu.kanade.tachiyomi.util.view.inflate
-import eu.kanade.tachiyomi.util.view.onAnimationsFinished
-import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import reactivecircus.flowbinding.recyclerview.scrollStateChanges
-import reactivecircus.flowbinding.swiperefreshlayout.refreshes
-import rx.subscriptions.CompositeSubscription
-import java.util.ArrayDeque
-
-/**
- * Fragment containing the library manga for a certain category.
- */
-class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    FrameLayout(context, attrs),
-    FlexibleAdapter.OnItemClickListener,
-    FlexibleAdapter.OnItemLongClickListener {
-
-    private val scope = MainScope()
-
-    /**
-     * The fragment containing this view.
-     */
-    private lateinit var controller: LibraryController
-
-    /**
-     * Category for this view.
-     */
-    lateinit var category: Category
-        private set
-
-    /**
-     * Recycler view of the list of manga.
-     */
-    private lateinit var recycler: AutofitRecyclerView
-
-    /**
-     * Adapter to hold the manga in this category.
-     */
-    private lateinit var adapter: LibraryCategoryAdapter
-
-    /**
-     * Subscriptions while the view is bound.
-     */
-    private var subscriptions = CompositeSubscription()
-
-    private var lastClickPositionStack = ArrayDeque(listOf(-1))
-
-    fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding, viewType: Int) {
-        this.controller = controller
-
-        recycler = if (viewType == LibraryAdapter.LIST_DISPLAY_MODE) {
-            (binding.swipeRefresh.inflate(R.layout.library_list_recycler) as AutofitRecyclerView).apply {
-                spanCount = 1
-            }
-        } else {
-            (binding.swipeRefresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
-                spanCount = controller.mangaPerRow
-            }
-        }
-
-        recycler.applyInsetter {
-            type(navigationBars = true) {
-                padding()
-            }
-        }
-
-        adapter = LibraryCategoryAdapter(this)
-
-        recycler.setHasFixedSize(true)
-        recycler.adapter = adapter
-        binding.swipeRefresh.addView(recycler)
-        adapter.fastScroller = binding.fastScroller
-
-        recycler.scrollStateChanges()
-            .onEach {
-                // Disable swipe refresh when view is not at the top
-                val firstPos = (recycler.layoutManager as LinearLayoutManager)
-                    .findFirstCompletelyVisibleItemPosition()
-                binding.swipeRefresh.isEnabled = firstPos <= 0
-            }
-            .launchIn(scope)
-
-        recycler.onAnimationsFinished {
-            (controller.activity as? MainActivity)?.ready = true
-        }
-
-        // Double the distance required to trigger sync
-        binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
-        binding.swipeRefresh.refreshes()
-            .onEach {
-                if (LibraryUpdateService.start(context, category)) {
-                    context.toast(R.string.updating_category)
-                }
-
-                // It can be a very long operation, so we disable swipe refresh and show a toast.
-                binding.swipeRefresh.isRefreshing = false
-            }
-            .launchIn(scope)
-    }
-
-    fun onBind(category: Category) {
-        this.category = category
-
-        adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
-            SelectableAdapter.Mode.MULTI
-        } else {
-            SelectableAdapter.Mode.SINGLE
-        }
-
-        subscriptions += controller.searchRelay
-            .doOnNext { adapter.setFilter(it) }
-            .skip(1)
-            .subscribe { adapter.performFilter() }
-
-        subscriptions += controller.libraryMangaRelay
-            .subscribe { onNextLibraryManga(it) }
-
-        subscriptions += controller.selectionRelay
-            .subscribe { onSelectionChanged(it) }
-
-        subscriptions += controller.selectAllRelay
-            .filter { it == category.id }
-            .subscribe {
-                adapter.currentItems.forEach { item ->
-                    controller.setSelection(item.manga.toDomainManga()!!, true)
-                }
-                controller.invalidateActionMode()
-            }
-
-        subscriptions += controller.selectInverseRelay
-            .filter { it == category.id }
-            .subscribe {
-                adapter.currentItems.forEach { item ->
-                    controller.toggleSelection(item.manga.toDomainManga()!!)
-                }
-                controller.invalidateActionMode()
-            }
-    }
-
-    fun onRecycle() {
-        adapter.setItems(emptyList())
-        adapter.clearSelection()
-        unsubscribe()
-    }
-
-    fun onDestroy() {
-        unsubscribe()
-        scope.cancel()
-    }
-
-    private fun unsubscribe() {
-        subscriptions.clear()
-    }
-
-    /**
-     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
-     * adapter.
-     *
-     * @param event the event received.
-     */
-    private fun onNextLibraryManga(event: LibraryMangaEvent) {
-        // Get the manga list for this category.
-        val mangaForCategory = event.getMangaForCategory(category).orEmpty()
-
-        // Update the category with its manga.
-        adapter.setItems(mangaForCategory)
-
-        if (adapter.mode == SelectableAdapter.Mode.MULTI) {
-            controller.selectedMangas.forEach { manga ->
-                val position = adapter.indexOf(manga)
-                if (position != -1 && !adapter.isSelected(position)) {
-                    adapter.toggleSelection(position)
-                    (recycler.findViewHolderForItemId(manga.id) as? LibraryHolder<*>)?.toggleActivation()
-                }
-            }
-        }
-    }
-
-    /**
-     * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
-     * depending on the type of event received.
-     *
-     * @param event the selection event received.
-     */
-    private fun onSelectionChanged(event: LibrarySelectionEvent) {
-        when (event) {
-            is LibrarySelectionEvent.Selected -> {
-                if (adapter.mode != SelectableAdapter.Mode.MULTI) {
-                    adapter.mode = SelectableAdapter.Mode.MULTI
-                }
-                findAndToggleSelection(event.manga)
-            }
-            is LibrarySelectionEvent.Unselected -> {
-                findAndToggleSelection(event.manga)
-
-                with(adapter.indexOf(event.manga)) {
-                    if (this != -1) lastClickPositionStack.remove(this)
-                }
-
-                if (controller.selectedMangas.isEmpty()) {
-                    adapter.mode = SelectableAdapter.Mode.SINGLE
-                }
-            }
-            is LibrarySelectionEvent.Cleared -> {
-                adapter.mode = SelectableAdapter.Mode.SINGLE
-                adapter.clearSelection()
-
-                lastClickPositionStack.clear()
-                lastClickPositionStack.push(-1)
-            }
-        }
-    }
-
-    /**
-     * Toggles the selection for the given manga and updates the view if needed.
-     *
-     * @param manga the manga to toggle.
-     */
-    private fun findAndToggleSelection(manga: Manga) {
-        val position = adapter.indexOf(manga)
-        if (position != -1) {
-            adapter.toggleSelection(position)
-            (recycler.findViewHolderForItemId(manga.id) as? LibraryHolder<*>)?.toggleActivation()
-        }
-    }
-
-    /**
-     * Called when a manga is clicked.
-     *
-     * @param position the position of the element clicked.
-     * @return true if the item should be selected, false otherwise.
-     */
-    override fun onItemClick(view: View?, position: Int): Boolean {
-        // If the action mode is created and the position is valid, toggle the selection.
-        val item = adapter.getItem(position) ?: return false
-        return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
-            if (adapter.isSelected(position)) {
-                lastClickPositionStack.remove(position)
-            } else {
-                lastClickPositionStack.push(position)
-            }
-            toggleSelection(position)
-            true
-        } else {
-            openManga(item.manga.toDomainManga()!!)
-            false
-        }
-    }
-
-    /**
-     * Called when a manga is long clicked.
-     *
-     * @param position the position of the element clicked.
-     */
-    override fun onItemLongClick(position: Int) {
-        controller.createActionModeIfNeeded()
-        val lastClickPosition = lastClickPositionStack.peek()!!
-        when {
-            lastClickPosition == -1 -> setSelection(position)
-            lastClickPosition > position ->
-                for (i in position until lastClickPosition)
-                    setSelection(i)
-            lastClickPosition < position ->
-                for (i in lastClickPosition + 1..position)
-                    setSelection(i)
-            else -> setSelection(position)
-        }
-        if (lastClickPosition != position) {
-            lastClickPositionStack.remove(position)
-            lastClickPositionStack.push(position)
-        }
-    }
-
-    /**
-     * Opens a manga.
-     *
-     * @param manga the manga to open.
-     */
-    private fun openManga(manga: Manga) {
-        controller.openManga(manga)
-    }
-
-    /**
-     * Tells the presenter to toggle the selection for the given position.
-     *
-     * @param position the position to toggle.
-     */
-    private fun toggleSelection(position: Int) {
-        val item = adapter.getItem(position) ?: return
-
-        controller.setSelection(item.manga.toDomainManga()!!, !adapter.isSelected(position))
-        controller.invalidateActionMode()
-    }
-
-    /**
-     * Tells the presenter to set the selection for the given position.
-     *
-     * @param position the position to toggle.
-     */
-    private fun setSelection(position: Int) {
-        val item = adapter.getItem(position) ?: return
-
-        controller.setSelection(item.manga.toDomainManga()!!, true)
-        controller.invalidateActionMode()
-    }
-}

+ 0 - 61
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt

@@ -1,61 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import coil.dispose
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
-import eu.kanade.tachiyomi.util.view.loadAutoPause
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "item_source_grid" are available in this class.
- *
- * @param binding the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new library holder.
- */
-class LibraryComfortableGridHolder(
-    override val binding: SourceComfortableGridItemBinding,
-    adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
-) : LibraryHolder<SourceComfortableGridItemBinding>(binding.root, adapter) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    override fun onSetValues(item: LibraryItem) {
-        // Update the title of the manga.
-        binding.title.text = item.manga.title
-
-        // For rounded corners
-        binding.badges.leftBadges.clipToOutline = true
-        binding.badges.rightBadges.clipToOutline = true
-
-        // Update the unread count and its visibility.
-        with(binding.badges.unreadText) {
-            isVisible = item.unreadCount > 0
-            text = item.unreadCount.toString()
-        }
-        // Update the download count and its visibility.
-        with(binding.badges.downloadText) {
-            isVisible = item.downloadCount > 0
-            text = item.downloadCount.toString()
-        }
-        // Update the source language and its visibility
-        with(binding.badges.languageText) {
-            isVisible = item.sourceLanguage.isNotEmpty()
-            text = item.sourceLanguage
-        }
-        // set local visibility if its local manga
-        binding.badges.localText.isVisible = item.isLocal
-
-        // Update the cover.
-        binding.thumbnail.dispose()
-        binding.thumbnail.loadAutoPause(item.manga)
-    }
-}

+ 0 - 72
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt

@@ -1,72 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import androidx.core.view.isVisible
-import coil.dispose
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
-import eu.kanade.tachiyomi.util.view.loadAutoPause
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "source_compact_grid_item" are available in this class.
- *
- * @param binding the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param coverOnly true if title should be hidden a.k.a cover only mode.
- * @constructor creates a new library holder.
- */
-class LibraryCompactGridHolder(
-    override val binding: SourceCompactGridItemBinding,
-    adapter: FlexibleAdapter<*>,
-    private val coverOnly: Boolean,
-) : LibraryHolder<SourceCompactGridItemBinding>(binding.root, adapter) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    override fun onSetValues(item: LibraryItem) {
-        // Update the title of the manga.
-        binding.title.text = item.manga.title
-
-        // For rounded corners
-        binding.badges.leftBadges.clipToOutline = true
-        binding.badges.rightBadges.clipToOutline = true
-
-        // Update the unread count and its visibility.
-        with(binding.badges.unreadText) {
-            isVisible = item.unreadCount > 0
-            text = item.unreadCount.toString()
-        }
-        // Update the download count and its visibility.
-        with(binding.badges.downloadText) {
-            isVisible = item.downloadCount > 0
-            text = item.downloadCount.toString()
-        }
-        // Update the source language and its visibility
-        with(binding.badges.languageText) {
-            isVisible = item.sourceLanguage.isNotEmpty()
-            text = item.sourceLanguage
-        }
-        // set local visibility if its local manga
-        binding.badges.localText.isVisible = item.isLocal
-
-        // Update the cover.
-        binding.thumbnail.dispose()
-        if (coverOnly) {
-            // Cover only mode: Hides title text unless thumbnail is unavailable
-            if (!item.manga.thumbnail_url.isNullOrEmpty()) {
-                binding.thumbnail.loadAutoPause(item.manga)
-                binding.title.isVisible = false
-            } else {
-                binding.title.text = item.manga.title
-                binding.title.isVisible = true
-            }
-            binding.thumbnail.foreground = null
-        } else {
-            binding.thumbnail.loadAutoPause(item.manga)
-        }
-    }
-}

+ 49 - 112
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -14,16 +14,15 @@ import com.bluelinelabs.conductor.ControllerChangeType
 import com.fredporciuncula.flow.preferences.Preference
 import com.google.android.material.tabs.TabLayout
 import com.jakewharton.rxrelay.BehaviorRelay
-import com.jakewharton.rxrelay.PublishRelay
 import eu.kanade.domain.category.model.Category
 import eu.kanade.domain.category.model.toDbCategory
 import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.toDbManga
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.toDomainManga
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
-import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.ui.base.controller.RootController
 import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
 import eu.kanade.tachiyomi.ui.base.controller.TabbedController
@@ -33,7 +32,6 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchUI
-import eu.kanade.tachiyomi.util.preference.asImmediateFlow
 import eu.kanade.tachiyomi.util.system.getResourceColor
 import eu.kanade.tachiyomi.util.system.openInBrowser
 import eu.kanade.tachiyomi.util.system.toast
@@ -73,42 +71,11 @@ class LibraryController(
      */
     private var actionMode: ActionModeWithToolbar? = null
 
-    /**
-     * Currently selected mangas.
-     */
-    val selectedMangas = mutableSetOf<Manga>()
-
-    /**
-     * Relay to notify the UI of selection updates.
-     */
-    val selectionRelay: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
-
-    /**
-     * Relay to notify search query changes.
-     */
-    val searchRelay: BehaviorRelay<String> = BehaviorRelay.create()
-
     /**
      * Relay to notify the library's viewpager for updates.
      */
     val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
 
-    /**
-     * Relay to notify the library's viewpager to select all manga
-     */
-    val selectAllRelay: PublishRelay<Long> = PublishRelay.create()
-
-    /**
-     * Relay to notify the library's viewpager to select the inverse
-     */
-    val selectInverseRelay: PublishRelay<Long> = PublishRelay.create()
-
-    /**
-     * Number of manga per row in grid mode.
-     */
-    var mangaPerRow = 0
-        private set
-
     /**
      * Adapter of the view pager.
      */
@@ -174,7 +141,19 @@ class LibraryController(
     override fun onViewCreated(view: View) {
         super.onViewCreated(view)
 
-        adapter = LibraryAdapter(this)
+        adapter = LibraryAdapter(
+            controller = this,
+            presenter = presenter,
+            onClickManga = {
+                openManga(it.id!!)
+            },
+        )
+
+        getColumnsPreferenceForCurrentOrientation()
+            .asFlow()
+            .onEach { presenter.columns = it }
+            .launchIn(viewScope)
+
         binding.libraryPager.adapter = adapter
         binding.libraryPager.pageSelections()
             .drop(1)
@@ -185,13 +164,7 @@ class LibraryController(
             }
             .launchIn(viewScope)
 
-        getColumnsPreferenceForCurrentOrientation().asImmediateFlow { mangaPerRow = it }
-            .drop(1)
-            // Set again the adapter to recalculate the covers height
-            .onEach { reattachAdapter() }
-            .launchIn(viewScope)
-
-        if (selectedMangas.isNotEmpty()) {
+        if (adapter!!.categories.isNotEmpty()) {
             createActionModeIfNeeded()
         }
 
@@ -219,6 +192,14 @@ class LibraryController(
             .launchIn(viewScope)
     }
 
+    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
+        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
+            preferences.portraitColumns()
+        } else {
+            preferences.landscapeColumns()
+        }
+    }
+
     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
         super.onChangeStarted(handler, type)
         if (type.isEnter) {
@@ -229,7 +210,6 @@ class LibraryController(
 
     override fun onDestroyView(view: View) {
         destroyActionModeIfNeeded()
-        adapter?.onDestroy()
         adapter = null
         settingsSheet?.sheetScope?.cancel()
         settingsSheet = null
@@ -313,6 +293,12 @@ class LibraryController(
             }
         }
 
+        presenter.loadedManga.clear()
+        mangaMap.forEach {
+            presenter.loadedManga[it.key] = it.value
+        }
+        presenter.loadedMangaFlow.value = presenter.loadedManga
+
         // Send the manga map to child fragments after the adapter is updated.
         libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
 
@@ -320,19 +306,6 @@ class LibraryController(
         updateTitle()
     }
 
-    /**
-     * Returns a preference for the number of manga per row based on the current orientation.
-     *
-     * @return the preference.
-     */
-    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
-        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
-            preferences.portraitColumns()
-        } else {
-            preferences.landscapeColumns()
-        }
-    }
-
     private fun onFilterChanged() {
         presenter.requestFilterUpdate()
         activity?.invalidateOptionsMenu()
@@ -400,7 +373,6 @@ class LibraryController(
     }
 
     private fun performSearch() {
-        searchRelay.call(presenter.query)
         if (presenter.query.isNotEmpty()) {
             binding.btnGlobalSearch.isVisible = true
             binding.btnGlobalSearch.text =
@@ -455,7 +427,7 @@ class LibraryController(
     }
 
     override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        val count = selectedMangas.size
+        val count = presenter.selection.size
         if (count == 0) {
             // Destroy action mode if there are no items selected.
             destroyActionModeIfNeeded()
@@ -466,9 +438,9 @@ class LibraryController(
     }
 
     override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
-        if (selectedMangas.isEmpty()) return
+        if (presenter.hasSelection().not()) return
         toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible =
-            selectedMangas.any { it.source != LocalSource.ID }
+            presenter.selection.any { presenter.loadedManga.values.any { it.any { it.isLocal } } }
     }
 
     override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
@@ -487,50 +459,18 @@ class LibraryController(
 
     override fun onDestroyActionMode(mode: ActionMode) {
         // Clear all the manga selections and notify child views.
-        selectedMangas.clear()
-        selectionRelay.call(LibrarySelectionEvent.Cleared)
+        presenter.clearSelection()
 
         (activity as? MainActivity)?.showBottomNav(true)
 
         actionMode = null
     }
 
-    fun openManga(manga: Manga) {
+    fun openManga(mangaId: Long) {
         // Notify the presenter a manga is being opened.
         presenter.onOpenManga()
 
-        router.pushController(MangaController(manga.id))
-    }
-
-    /**
-     * Sets the selection for a given manga.
-     *
-     * @param manga the manga whose selection has changed.
-     * @param selected whether it's now selected or not.
-     */
-    fun setSelection(manga: Manga, selected: Boolean) {
-        if (selected) {
-            if (selectedMangas.add(manga)) {
-                selectionRelay.call(LibrarySelectionEvent.Selected(manga))
-            }
-        } else {
-            if (selectedMangas.remove(manga)) {
-                selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
-            }
-        }
-    }
-
-    /**
-     * Toggles the current selection state for a given manga.
-     *
-     * @param manga the manga whose selection to change.
-     */
-    fun toggleSelection(manga: Manga) {
-        if (selectedMangas.add(manga)) {
-            selectionRelay.call(LibrarySelectionEvent.Selected(manga))
-        } else if (selectedMangas.remove(manga)) {
-            selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
-        }
+        router.pushController(MangaController(mangaId))
     }
 
     /**
@@ -538,8 +478,7 @@ class LibraryController(
      * invalidate the action mode to revert the top toolbar
      */
     fun clearSelection() {
-        selectedMangas.clear()
-        selectionRelay.call(LibrarySelectionEvent.Cleared)
+        presenter.clearSelection()
         invalidateActionMode()
     }
 
@@ -549,15 +488,15 @@ class LibraryController(
     private fun showMangaCategoriesDialog() {
         viewScope.launchIO {
             // Create a copy of selected manga
-            val mangas = selectedMangas.toList()
+            val mangas = presenter.selection.toList()
 
             // Hide the default category because it has a different behavior than the ones from db.
             val categories = presenter.categories.filter { it.id != 0L }
 
             // Get indexes of the common categories to preselect.
-            val common = presenter.getCommonCategories(mangas)
+            val common = presenter.getCommonCategories(mangas.mapNotNull { it.toDomainManga() })
             // Get indexes of the mix categories to preselect.
-            val mix = presenter.getMixCategories(mangas)
+            val mix = presenter.getMixCategories(mangas.mapNotNull { it.toDomainManga() })
             val preselected = categories.map {
                 when (it) {
                     in common -> QuadStateTextView.State.CHECKED.ordinal
@@ -566,26 +505,27 @@ class LibraryController(
                 }
             }.toTypedArray()
             launchUI {
-                ChangeMangaCategoriesDialog(this@LibraryController, mangas, categories, preselected)
+                ChangeMangaCategoriesDialog(this@LibraryController, mangas.mapNotNull { it.toDomainManga() }, categories, preselected)
                     .showDialog(router)
             }
         }
     }
 
     private fun downloadUnreadChapters() {
-        val mangas = selectedMangas.toList()
-        presenter.downloadUnreadChapters(mangas)
+        val mangas = presenter.selection.toList()
+        presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() })
         destroyActionModeIfNeeded()
     }
 
     private fun markReadStatus(read: Boolean) {
-        val mangas = selectedMangas.toList()
-        presenter.markReadStatus(mangas, read)
+        val mangas = presenter.selection.toList()
+        presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read)
         destroyActionModeIfNeeded()
     }
 
     private fun showDeleteMangaDialog() {
-        DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
+        val mangas = presenter.selection.toList()
+        DeleteLibraryMangasDialog(this, mangas.mapNotNull { it.toDomainManga() }).showDialog(router)
     }
 
     override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
@@ -599,21 +539,18 @@ class LibraryController(
     }
 
     private fun selectAllCategoryManga() {
-        adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
-            selectAllRelay.call(it)
-        }
+        presenter.selectAll(binding.libraryPager.currentItem)
     }
 
     private fun selectInverseCategoryManga() {
-        adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
-            selectInverseRelay.call(it)
-        }
+        presenter.invertSelection(binding.libraryPager.currentItem)
     }
 
     override fun onSearchViewQueryTextChange(newText: String?) {
         // Ignore events if this controller isn't at the top to avoid query being reset
         if (router.backstack.lastOrNull()?.controller == this) {
             presenter.query = newText ?: ""
+            presenter.searchQuery = newText ?: ""
             performSearch()
         }
     }

+ 0 - 29
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt

@@ -1,29 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import androidx.viewbinding.ViewBinding
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.viewholders.FlexibleViewHolder
-
-/**
- * Generic class used to hold the displayed data of a manga in the library.
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to the single tap and long tap events.
- */
-
-abstract class LibraryHolder<VB : ViewBinding>(
-    view: View,
-    adapter: FlexibleAdapter<*>,
-) : FlexibleViewHolder(view, adapter) {
-
-    abstract val binding: VB
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    abstract fun onSetValues(item: LibraryItem)
-}

+ 2 - 58
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<Boolean>,
-    private val defaultLibraryDisplayMode: Preference<DisplayModeSetting>,
-) :
-    AbstractFlexibleItem<LibraryHolder<*>>(), IFilterable<String> {
+) {
 
     private val sourceManager: SourceManager = Injekt.get()
 
@@ -31,55 +17,13 @@ class LibraryItem(
     var isLocal = false
     var sourceLanguage = ""
 
-    private fun getDisplayMode(): DisplayModeSetting {
-        return if (shouldSetFromCategory.get() && manga.category != 0) {
-            DisplayModeSetting.fromFlag(displayMode)
-        } else {
-            defaultLibraryDisplayMode.get()
-        }
-    }
-
-    override fun getLayoutRes(): Int {
-        return when (getDisplayMode()) {
-            DisplayModeSetting.COMPACT_GRID, DisplayModeSetting.COVER_ONLY_GRID -> R.layout.source_compact_grid_item
-            DisplayModeSetting.COMFORTABLE_GRID -> R.layout.source_comfortable_grid_item
-            DisplayModeSetting.LIST -> R.layout.source_list_item
-        }
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder<*> {
-        return when (getDisplayMode()) {
-            DisplayModeSetting.COMPACT_GRID -> {
-                LibraryCompactGridHolder(SourceCompactGridItemBinding.bind(view), adapter, coverOnly = false)
-            }
-            DisplayModeSetting.COVER_ONLY_GRID -> {
-                LibraryCompactGridHolder(SourceCompactGridItemBinding.bind(view), adapter, coverOnly = true)
-            }
-            DisplayModeSetting.COMFORTABLE_GRID -> {
-                LibraryComfortableGridHolder(SourceComfortableGridItemBinding.bind(view), adapter)
-            }
-            DisplayModeSetting.LIST -> {
-                LibraryListHolder(view, adapter)
-            }
-        }
-    }
-
-    override fun bindViewHolder(
-        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
-        holder: LibraryHolder<*>,
-        position: Int,
-        payloads: List<Any?>?,
-    ) {
-        holder.onSetValues(this)
-    }
-
     /**
      * Filters a manga depending on a query.
      *
      * @param constraint the query to apply.
      * @return true if the manga should be included, false otherwise.
      */
-    override fun filter(constraint: String): Boolean {
+    fun filter(constraint: String): Boolean {
         val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
         val genres by lazy { manga.getGenres() }
         return manga.title.contains(constraint, true) ||

+ 0 - 67
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt

@@ -1,67 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import androidx.core.view.isVisible
-import coil.dispose
-import coil.load
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.databinding.SourceListItemBinding
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "item_library_list" are available in this class.
- *
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new library holder.
- */
-class LibraryListHolder(
-    private val view: View,
-    private val adapter: FlexibleAdapter<*>,
-) : LibraryHolder<SourceListItemBinding>(view, adapter) {
-
-    override val binding = SourceListItemBinding.bind(view)
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    override fun onSetValues(item: LibraryItem) {
-        // Update the title of the manga.
-        binding.title.text = item.manga.title
-
-        // For rounded corners
-        binding.badges.clipToOutline = true
-
-        // Update the unread count and its visibility.
-        with(binding.unreadText) {
-            isVisible = item.unreadCount > 0
-            text = item.unreadCount.toString()
-        }
-        // Update the download count and its visibility.
-        with(binding.downloadText) {
-            isVisible = item.downloadCount > 0
-            text = "${item.downloadCount}"
-        }
-        // Update the source language and its visibility
-        with(binding.languageText) {
-            isVisible = item.sourceLanguage.isNotEmpty()
-            text = item.sourceLanguage
-        }
-        // show local text badge if local manga
-        binding.localText.isVisible = item.isLocal
-
-        // Create thumbnail onclick to simulate long click
-        binding.thumbnail.setOnClickListener {
-            // Simulate long click on this view to enter selection mode
-            onLongClick(itemView)
-        }
-
-        // Update the cover
-        binding.thumbnail.dispose()
-        binding.thumbnail.load(item.manga)
-    }
-}

+ 106 - 6
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.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.util.fastAny
 import com.jakewharton.rxrelay.BehaviorRelay
 import eu.kanade.core.util.asObservable
 import eu.kanade.data.DatabaseHandler
@@ -18,6 +27,7 @@ import eu.kanade.domain.manga.model.MangaUpdate
 import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.domain.track.interactor.GetTracks
 import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.data.database.models.LibraryManga
 import eu.kanade.tachiyomi.data.database.models.toDomainManga
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -26,6 +36,7 @@ import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.model.SManga
 import eu.kanade.tachiyomi.source.online.HttpSource
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
 import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
 import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
 import eu.kanade.tachiyomi.util.lang.combineLatest
@@ -33,6 +44,12 @@ import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.removeCovers
 import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
 import rx.Observable
 import rx.Subscription
@@ -80,9 +97,24 @@ class LibraryPresenter(
     /**
      * Categories of the library.
      */
-    var categories: List<Category> = emptyList()
+    var categories: List<Category> = mutableStateListOf()
         private set
 
+    var loadedManga = mutableStateMapOf<Long, List<LibraryItem>>()
+        private set
+
+    val loadedMangaFlow = MutableStateFlow(loadedManga)
+
+    var searchQuery by mutableStateOf(query)
+
+    val selection: MutableList<LibraryManga> = mutableStateListOf()
+
+    val isPerCategory by mutableStateOf(preferences.categorizedDisplaySettings().get())
+
+    var columns by mutableStateOf(0)
+
+    var currentDisplayMode by mutableStateOf(preferences.libraryDisplayMode().get())
+
     /**
      * Relay used to apply the UI filters to the last emission of the library.
      */
@@ -105,6 +137,14 @@ class LibraryPresenter(
 
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
+        preferences.libraryDisplayMode()
+            .asFlow()
+            .drop(1)
+            .onEach {
+                currentDisplayMode = it
+            }
+            .launchIn(presenterScope)
+
         subscribeLibrary()
     }
 
@@ -416,11 +456,7 @@ class LibraryPresenter(
             .map { list ->
                 list.map { libraryManga ->
                     // Display mode based on user preference: take it from global library setting or category
-                    LibraryItem(
-                        libraryManga,
-                        shouldSetFromCategory,
-                        defaultLibraryDisplayMode,
-                    )
+                    LibraryItem(libraryManga)
                 }.groupBy { it.manga.category.toLong() }
             }
     }
@@ -592,4 +628,68 @@ class LibraryPresenter(
             }
         }
     }
+
+    @Composable
+    fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State<List<LibraryItem>> {
+        val unfiltered = loadedManga[categoryId] ?: emptyList()
+
+        return derivedStateOf {
+            val query = searchQuery
+            if (query.isNotBlank()) {
+                unfiltered.filter {
+                    it.filter(query)
+                }
+            } else {
+                unfiltered
+            }
+        }
+    }
+
+    @Composable
+    fun getDisplayMode(index: Int): DisplayModeSetting {
+        val category = categories[index]
+        return remember {
+            if (isPerCategory.not() || category.id == 0L) {
+                currentDisplayMode
+            } else {
+                DisplayModeSetting.fromFlag(category.displayMode)
+            }
+        }
+    }
+
+    fun hasSelection(): Boolean {
+        return selection.isNotEmpty()
+    }
+
+    fun clearSelection() {
+        selection.clear()
+    }
+
+    fun toggleSelection(manga: LibraryManga) {
+        if (selection.fastAny { it.id == manga.id }) {
+            selection.remove(manga)
+        } else {
+            selection.add(manga)
+        }
+        view?.invalidateActionMode()
+        view?.createActionModeIfNeeded()
+    }
+
+    fun selectAll(index: Int) {
+        val category = categories[index]
+        val items = loadedManga[category.id] ?: emptyList()
+        selection.addAll(items.filterNot { it.manga in selection }.map { it.manga })
+        view?.createActionModeIfNeeded()
+        view?.invalidateActionMode()
+    }
+
+    fun invertSelection(index: Int) {
+        val category = categories[index]
+        val items = (loadedManga[category.id] ?: emptyList()).map { it.manga }
+        val invert = items.filterNot { it in selection }
+        selection.removeAll(items)
+        selection.addAll(invert)
+        view?.createActionModeIfNeeded()
+        view?.invalidateActionMode()
+    }
 }

+ 0 - 9
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt

@@ -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()
-}

+ 0 - 22
app/src/main/res/layout/library_category.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<eu.kanade.tachiyomi.ui.library.LibraryCategoryView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
-        android:id="@+id/swipe_refresh"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
-    <eu.kanade.tachiyomi.widget.MaterialFastScroll
-        android:id="@+id/fast_scroller"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_centerHorizontal="true"
-        android:layout_gravity="end"
-        app:fastScrollerBubbleEnabled="false"
-        tools:visibility="visible" />
-
-</eu.kanade.tachiyomi.ui.library.LibraryCategoryView>

+ 0 - 14
app/src/main/res/layout/library_grid_recycler.xml

@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<eu.kanade.tachiyomi.widget.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/library_grid"
-    style="@style/Widget.Tachiyomi.GridView.Source"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:clipToPadding="false"
-    android:columnWidth="140dp"
-    android:paddingStart="5dp"
-    android:paddingTop="5dp"
-    android:paddingEnd="5dp"
-    android:paddingBottom="@dimen/action_toolbar_list_padding"
-    tools:listitem="@layout/source_compact_grid_item" />

+ 0 - 9
app/src/main/res/layout/library_list_recycler.xml

@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<eu.kanade.tachiyomi.widget.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/library_list"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:clipToPadding="false"
-    android:paddingBottom="@dimen/action_toolbar_list_padding"
-    tools:listitem="@layout/source_list_item" />