mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Use Compose for Library screen (#7557)
- Move Pager to Compose - Move AppBar to Compose - Use Stable interface for state - Use pills for no. of manga in category instead of (x)
This commit is contained in:
		@@ -38,8 +38,8 @@ fun Badge(
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .background(color)
 | 
			
		||||
            .clip(shape),
 | 
			
		||||
            .clip(shape)
 | 
			
		||||
            .background(color),
 | 
			
		||||
    ) {
 | 
			
		||||
        Text(
 | 
			
		||||
            text = text,
 | 
			
		||||
 
 | 
			
		||||
@@ -195,3 +195,91 @@ private fun RowScope.Button(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryBottomActionMenu(
 | 
			
		||||
    visible: Boolean,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    onChangeCategoryClicked: (() -> Unit)?,
 | 
			
		||||
    onMarkAsReadClicked: (() -> Unit)?,
 | 
			
		||||
    onMarkAsUnreadClicked: (() -> Unit)?,
 | 
			
		||||
    onDownloadClicked: (() -> Unit)?,
 | 
			
		||||
    onDeleteClicked: (() -> Unit)?,
 | 
			
		||||
) {
 | 
			
		||||
    AnimatedVisibility(
 | 
			
		||||
        visible = visible,
 | 
			
		||||
        enter = expandVertically(expandFrom = Alignment.Bottom),
 | 
			
		||||
        exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
 | 
			
		||||
    ) {
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
        Surface(
 | 
			
		||||
            modifier = modifier,
 | 
			
		||||
            shape = MaterialTheme.shapes.large,
 | 
			
		||||
            tonalElevation = 3.dp,
 | 
			
		||||
        ) {
 | 
			
		||||
            val haptic = LocalHapticFeedback.current
 | 
			
		||||
            val confirm = remember { mutableStateListOf(false, false, false, false, false) }
 | 
			
		||||
            var resetJob: Job? = remember { null }
 | 
			
		||||
            val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
 | 
			
		||||
                haptic.performHapticFeedback(HapticFeedbackType.LongPress)
 | 
			
		||||
                (0 until 5).forEach { i -> confirm[i] = i == toConfirmIndex }
 | 
			
		||||
                resetJob?.cancel()
 | 
			
		||||
                resetJob = scope.launch {
 | 
			
		||||
                    delay(1000)
 | 
			
		||||
                    if (isActive) confirm[toConfirmIndex] = false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .navigationBarsPadding()
 | 
			
		||||
                    .padding(horizontal = 8.dp, vertical = 12.dp),
 | 
			
		||||
            ) {
 | 
			
		||||
                if (onChangeCategoryClicked != null) {
 | 
			
		||||
                    Button(
 | 
			
		||||
                        title = stringResource(R.string.action_move_category),
 | 
			
		||||
                        icon = Icons.Default.BookmarkAdd,
 | 
			
		||||
                        toConfirm = confirm[0],
 | 
			
		||||
                        onLongClick = { onLongClickItem(0) },
 | 
			
		||||
                        onClick = onChangeCategoryClicked,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                if (onMarkAsReadClicked != null) {
 | 
			
		||||
                    Button(
 | 
			
		||||
                        title = stringResource(R.string.action_mark_as_read),
 | 
			
		||||
                        icon = Icons.Default.DoneAll,
 | 
			
		||||
                        toConfirm = confirm[1],
 | 
			
		||||
                        onLongClick = { onLongClickItem(1) },
 | 
			
		||||
                        onClick = onMarkAsReadClicked,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                if (onMarkAsUnreadClicked != null) {
 | 
			
		||||
                    Button(
 | 
			
		||||
                        title = stringResource(R.string.action_mark_as_unread),
 | 
			
		||||
                        icon = Icons.Default.RemoveDone,
 | 
			
		||||
                        toConfirm = confirm[2],
 | 
			
		||||
                        onLongClick = { onLongClickItem(2) },
 | 
			
		||||
                        onClick = onMarkAsUnreadClicked,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                if (onDownloadClicked != null) {
 | 
			
		||||
                    Button(
 | 
			
		||||
                        title = stringResource(R.string.action_download),
 | 
			
		||||
                        icon = Icons.Outlined.Download,
 | 
			
		||||
                        toConfirm = confirm[3],
 | 
			
		||||
                        onLongClick = { onLongClickItem(3) },
 | 
			
		||||
                        onClick = onDownloadClicked,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                if (onDeleteClicked != null) {
 | 
			
		||||
                    Button(
 | 
			
		||||
                        title = stringResource(R.string.action_delete),
 | 
			
		||||
                        icon = Icons.Outlined.Delete,
 | 
			
		||||
                        toConfirm = confirm[4],
 | 
			
		||||
                        onLongClick = { onLongClickItem(4) },
 | 
			
		||||
                        onClick = onDeleteClicked,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								app/src/main/java/eu/kanade/presentation/components/Pill.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/src/main/java/eu/kanade/presentation/components/Pill.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.LocalTextStyle
 | 
			
		||||
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.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.TextUnit
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun Pill(
 | 
			
		||||
    text: String,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    color: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.background,
 | 
			
		||||
    contentColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onBackground,
 | 
			
		||||
    elevation: Dp = 1.dp,
 | 
			
		||||
    fontSize: TextUnit = LocalTextStyle.current.fontSize,
 | 
			
		||||
) {
 | 
			
		||||
    androidx.compose.material3.Surface(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .padding(start = 4.dp)
 | 
			
		||||
            .clip(RoundedCornerShape(100)),
 | 
			
		||||
        color = color,
 | 
			
		||||
        contentColor = contentColor,
 | 
			
		||||
        tonalElevation = elevation,
 | 
			
		||||
    ) {
 | 
			
		||||
        Text(
 | 
			
		||||
            text = text,
 | 
			
		||||
            modifier = Modifier.padding(6.dp, 1.dp),
 | 
			
		||||
            fontSize = fontSize,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
package eu.kanade.presentation.library
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import eu.kanade.presentation.components.LibraryBottomActionMenu
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.presentation.library.components.LibraryContent
 | 
			
		||||
import eu.kanade.presentation.library.components.LibraryToolbar
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryScreen(
 | 
			
		||||
    presenter: LibraryPresenter,
 | 
			
		||||
    onMangaClicked: (Long) -> Unit,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
    onChangeCategoryClicked: () -> Unit,
 | 
			
		||||
    onMarkAsReadClicked: () -> Unit,
 | 
			
		||||
    onMarkAsUnreadClicked: () -> Unit,
 | 
			
		||||
    onDownloadClicked: () -> Unit,
 | 
			
		||||
    onDeleteClicked: () -> Unit,
 | 
			
		||||
    onClickUnselectAll: () -> Unit,
 | 
			
		||||
    onClickSelectAll: () -> Unit,
 | 
			
		||||
    onClickInvertSelection: () -> Unit,
 | 
			
		||||
    onClickFilter: () -> Unit,
 | 
			
		||||
    onClickRefresh: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = {
 | 
			
		||||
            val title by presenter.getToolbarTitle()
 | 
			
		||||
            LibraryToolbar(
 | 
			
		||||
                state = presenter,
 | 
			
		||||
                title = title,
 | 
			
		||||
                onClickUnselectAll = onClickUnselectAll,
 | 
			
		||||
                onClickSelectAll = onClickSelectAll,
 | 
			
		||||
                onClickInvertSelection = onClickInvertSelection,
 | 
			
		||||
                onClickFilter = onClickFilter,
 | 
			
		||||
                onClickRefresh = onClickRefresh,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        bottomBar = {
 | 
			
		||||
            LibraryBottomActionMenu(
 | 
			
		||||
                visible = presenter.selectionMode,
 | 
			
		||||
                onChangeCategoryClicked = onChangeCategoryClicked,
 | 
			
		||||
                onMarkAsReadClicked = onMarkAsReadClicked,
 | 
			
		||||
                onMarkAsUnreadClicked = onMarkAsUnreadClicked,
 | 
			
		||||
                onDownloadClicked = onDownloadClicked,
 | 
			
		||||
                onDeleteClicked = onDeleteClicked,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        LibraryContent(
 | 
			
		||||
            state = presenter,
 | 
			
		||||
            contentPadding = paddingValues,
 | 
			
		||||
            currentPage = presenter.activeCategory,
 | 
			
		||||
            isLibraryEmpty = presenter.loadedManga.isEmpty(),
 | 
			
		||||
            showPageTabs = presenter.tabVisibility,
 | 
			
		||||
            showMangaCount = presenter.mangaCountVisibility,
 | 
			
		||||
            onChangeCurrentPage = { presenter.activeCategory = it },
 | 
			
		||||
            onMangaClicked = onMangaClicked,
 | 
			
		||||
            onToggleSelection = { presenter.toggleSelection(it) },
 | 
			
		||||
            onRefresh = onClickRefresh,
 | 
			
		||||
            onGlobalSearchClicked = onGlobalSearchClicked,
 | 
			
		||||
            getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) },
 | 
			
		||||
            getDisplayModeForPage = { presenter.getDisplayMode(index = it) },
 | 
			
		||||
            getColumnsForOrientation = { presenter.getColumnsPreferenceForCurrentOrientation(it) },
 | 
			
		||||
            getLibraryForPage = { presenter.getMangaForCategory(page = it) },
 | 
			
		||||
            isIncognitoMode = presenter.isIncognitoMode,
 | 
			
		||||
            isDownloadOnly = presenter.isDownloadOnly,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package eu.kanade.presentation.library
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.derivedStateOf
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import eu.kanade.domain.category.model.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
interface LibraryState {
 | 
			
		||||
    val isLoading: Boolean
 | 
			
		||||
    val categories: List<Category>
 | 
			
		||||
    var searchQuery: String?
 | 
			
		||||
    val selection: List<LibraryManga>
 | 
			
		||||
    val selectionMode: Boolean
 | 
			
		||||
    var hasActiveFilters: Boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun LibraryState(): LibraryState {
 | 
			
		||||
    return LibraryStateImpl()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class LibraryStateImpl : LibraryState {
 | 
			
		||||
    override var isLoading: Boolean by mutableStateOf(true)
 | 
			
		||||
    override var categories: List<Category> by mutableStateOf(emptyList())
 | 
			
		||||
    override var searchQuery: String? by mutableStateOf(null)
 | 
			
		||||
    override var selection: List<LibraryManga> by mutableStateOf(emptyList())
 | 
			
		||||
    override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() }
 | 
			
		||||
    override var hasActiveFilters: Boolean by mutableStateOf(false)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,14 +3,19 @@ package eu.kanade.presentation.library.components
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
 | 
			
		||||
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.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.zIndex
 | 
			
		||||
import eu.kanade.domain.manga.model.MangaCover
 | 
			
		||||
import eu.kanade.presentation.components.TextButton
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
 | 
			
		||||
@@ -21,10 +26,22 @@ fun LibraryComfortableGrid(
 | 
			
		||||
    selection: List<LibraryManga>,
 | 
			
		||||
    onClick: (LibraryManga) -> Unit,
 | 
			
		||||
    onLongClick: (LibraryManga) -> Unit,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyLibraryGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
    ) {
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (searchQuery.isNullOrEmpty().not()) {
 | 
			
		||||
                TextButton(onClick = onGlobalSearchClicked) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = stringResource(R.string.action_global_search_query, searchQuery!!),
 | 
			
		||||
                        modifier = Modifier.zIndex(99f),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        items(
 | 
			
		||||
            items = items,
 | 
			
		||||
            key = {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ 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.GridItemSpan
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.items
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.LocalTextStyle
 | 
			
		||||
@@ -17,8 +18,12 @@ import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.graphics.Brush
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.Shadow
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.zIndex
 | 
			
		||||
import eu.kanade.presentation.components.TextButton
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
 | 
			
		||||
@@ -29,10 +34,23 @@ fun LibraryCompactGrid(
 | 
			
		||||
    selection: List<LibraryManga>,
 | 
			
		||||
    onClick: (LibraryManga) -> Unit,
 | 
			
		||||
    onLongClick: (LibraryManga) -> Unit,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyLibraryGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
    ) {
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (searchQuery.isNullOrEmpty().not()) {
 | 
			
		||||
                TextButton(onClick = onGlobalSearchClicked) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = stringResource(R.string.action_global_search_query, searchQuery!!),
 | 
			
		||||
                        modifier = Modifier.zIndex(99f),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(
 | 
			
		||||
            items = items,
 | 
			
		||||
            key = {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,126 @@
 | 
			
		||||
package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
 | 
			
		||||
import com.google.accompanist.pager.rememberPagerState
 | 
			
		||||
import com.google.accompanist.swiperefresh.SwipeRefresh
 | 
			
		||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
 | 
			
		||||
import eu.kanade.core.prefs.PreferenceMutableState
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.LoadingScreen
 | 
			
		||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
 | 
			
		||||
import eu.kanade.presentation.library.LibraryState
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.openInBrowser
 | 
			
		||||
import eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryContent(
 | 
			
		||||
    state: LibraryState,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    currentPage: Int,
 | 
			
		||||
    isLibraryEmpty: Boolean,
 | 
			
		||||
    isDownloadOnly: Boolean,
 | 
			
		||||
    isIncognitoMode: Boolean,
 | 
			
		||||
    showPageTabs: Boolean,
 | 
			
		||||
    showMangaCount: Boolean,
 | 
			
		||||
    onChangeCurrentPage: (Int) -> Unit,
 | 
			
		||||
    onMangaClicked: (Long) -> Unit,
 | 
			
		||||
    onToggleSelection: (LibraryManga) -> Unit,
 | 
			
		||||
    onRefresh: () -> Unit,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
    getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
 | 
			
		||||
    getDisplayModeForPage: @Composable (Int) -> State<DisplayModeSetting>,
 | 
			
		||||
    getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
 | 
			
		||||
    getLibraryForPage: @Composable (Int) -> State<List<LibraryItem>>,
 | 
			
		||||
) {
 | 
			
		||||
    val nestedScrollInterop = rememberNestedScrollInteropConnection()
 | 
			
		||||
 | 
			
		||||
    val pagerState = rememberPagerState(currentPage)
 | 
			
		||||
 | 
			
		||||
    val categories = state.categories
 | 
			
		||||
 | 
			
		||||
    if (categories.isEmpty()) {
 | 
			
		||||
        LoadingScreen()
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = Modifier.padding(contentPadding),
 | 
			
		||||
    ) {
 | 
			
		||||
        if (showPageTabs && categories.size > 1) {
 | 
			
		||||
            LibraryTabs(
 | 
			
		||||
                state = pagerState,
 | 
			
		||||
                categories = state.categories,
 | 
			
		||||
                showMangaCount = showMangaCount,
 | 
			
		||||
                getNumberOfMangaForCategory = getNumberOfMangaForCategory,
 | 
			
		||||
                isDownloadOnly = isDownloadOnly,
 | 
			
		||||
                isIncognitoMode = isIncognitoMode,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val onClickManga = { manga: LibraryManga ->
 | 
			
		||||
            if (state.selectionMode.not()) {
 | 
			
		||||
                onMangaClicked(manga.id!!)
 | 
			
		||||
            } else {
 | 
			
		||||
                onToggleSelection(manga)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        val onLongClickManga = { manga: LibraryManga ->
 | 
			
		||||
            onToggleSelection(manga)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        SwipeRefresh(
 | 
			
		||||
            state = rememberSwipeRefreshState(isRefreshing = false),
 | 
			
		||||
            modifier = Modifier.nestedScroll(nestedScrollInterop),
 | 
			
		||||
            onRefresh = onRefresh,
 | 
			
		||||
            indicator = { s, trigger ->
 | 
			
		||||
                SwipeRefreshIndicator(
 | 
			
		||||
                    state = s,
 | 
			
		||||
                    refreshTriggerDistance = trigger,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
        ) {
 | 
			
		||||
            if (state.searchQuery.isNullOrEmpty() && isLibraryEmpty) {
 | 
			
		||||
                val context = LocalContext.current
 | 
			
		||||
                EmptyScreen(
 | 
			
		||||
                    R.string.information_empty_library,
 | 
			
		||||
                    listOf(
 | 
			
		||||
                        EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) {
 | 
			
		||||
                            context.openInBrowser("https://tachiyomi.org/help/guides/getting-started")
 | 
			
		||||
                        },
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                return@SwipeRefresh
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LibraryPager(
 | 
			
		||||
                state = pagerState,
 | 
			
		||||
                pageCount = categories.size,
 | 
			
		||||
                selectedManga = state.selection,
 | 
			
		||||
                getDisplayModeForPage = getDisplayModeForPage,
 | 
			
		||||
                getColumnsForOrientation = getColumnsForOrientation,
 | 
			
		||||
                getLibraryForPage = getLibraryForPage,
 | 
			
		||||
                onClickManga = onClickManga,
 | 
			
		||||
                onLongClickManga = onLongClickManga,
 | 
			
		||||
                onGlobalSearchClicked = onGlobalSearchClicked,
 | 
			
		||||
                searchQuery = state.searchQuery,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(pagerState.currentPage) {
 | 
			
		||||
            onChangeCurrentPage(pagerState.currentPage)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,15 @@
 | 
			
		||||
package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.items
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.zIndex
 | 
			
		||||
import eu.kanade.presentation.components.TextButton
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
 | 
			
		||||
@@ -14,10 +20,22 @@ fun LibraryCoverOnlyGrid(
 | 
			
		||||
    selection: List<LibraryManga>,
 | 
			
		||||
    onClick: (LibraryManga) -> Unit,
 | 
			
		||||
    onLongClick: (LibraryManga) -> Unit,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyLibraryGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
    ) {
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (searchQuery.isNullOrEmpty().not()) {
 | 
			
		||||
                TextButton(onClick = onGlobalSearchClicked) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = stringResource(R.string.action_global_search_query, searchQuery!!),
 | 
			
		||||
                        modifier = Modifier.zIndex(99f),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        items(
 | 
			
		||||
            items = items,
 | 
			
		||||
            key = {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,11 @@ import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.zIndex
 | 
			
		||||
import eu.kanade.domain.manga.model.MangaCover
 | 
			
		||||
import eu.kanade.presentation.components.Badge
 | 
			
		||||
import eu.kanade.presentation.components.BadgeGroup
 | 
			
		||||
import eu.kanade.presentation.components.TextButton
 | 
			
		||||
import eu.kanade.presentation.util.horizontalPadding
 | 
			
		||||
import eu.kanade.presentation.util.selectedBackground
 | 
			
		||||
import eu.kanade.presentation.util.verticalPadding
 | 
			
		||||
@@ -33,10 +35,23 @@ fun LibraryList(
 | 
			
		||||
    selection: List<LibraryManga>,
 | 
			
		||||
    onClick: (LibraryManga) -> Unit,
 | 
			
		||||
    onLongClick: (LibraryManga) -> Unit,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        contentPadding = WindowInsets.navigationBars.asPaddingValues(),
 | 
			
		||||
    ) {
 | 
			
		||||
        item {
 | 
			
		||||
            if (searchQuery.isNullOrEmpty().not()) {
 | 
			
		||||
                TextButton(onClick = onGlobalSearchClicked) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = stringResource(R.string.action_global_search_query, searchQuery!!),
 | 
			
		||||
                        modifier = Modifier.zIndex(99f),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(
 | 
			
		||||
            items = items,
 | 
			
		||||
            key = {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,96 @@
 | 
			
		||||
package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
			
		||||
import com.google.accompanist.pager.HorizontalPager
 | 
			
		||||
import com.google.accompanist.pager.PagerState
 | 
			
		||||
import eu.kanade.core.prefs.PreferenceMutableState
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryPager(
 | 
			
		||||
    state: PagerState,
 | 
			
		||||
    pageCount: Int,
 | 
			
		||||
    selectedManga: List<LibraryManga>,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    onGlobalSearchClicked: () -> Unit,
 | 
			
		||||
    getDisplayModeForPage: @Composable (Int) -> State<DisplayModeSetting>,
 | 
			
		||||
    getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
 | 
			
		||||
    getLibraryForPage: @Composable (Int) -> State<List<LibraryItem>>,
 | 
			
		||||
    onClickManga: (LibraryManga) -> Unit,
 | 
			
		||||
    onLongClickManga: (LibraryManga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    HorizontalPager(
 | 
			
		||||
        count = pageCount,
 | 
			
		||||
        modifier = Modifier.fillMaxSize(),
 | 
			
		||||
        state = state,
 | 
			
		||||
        verticalAlignment = Alignment.Top,
 | 
			
		||||
    ) { page ->
 | 
			
		||||
        val library by getLibraryForPage(page)
 | 
			
		||||
        val displayMode by getDisplayModeForPage(page)
 | 
			
		||||
        val columns by if (displayMode != DisplayModeSetting.LIST) {
 | 
			
		||||
            val configuration = LocalConfiguration.current
 | 
			
		||||
            val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
 | 
			
		||||
 | 
			
		||||
            remember(isLandscape) { getColumnsForOrientation(isLandscape) }
 | 
			
		||||
        } else {
 | 
			
		||||
            remember { mutableStateOf(0) }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        when (displayMode) {
 | 
			
		||||
            DisplayModeSetting.LIST -> {
 | 
			
		||||
                LibraryList(
 | 
			
		||||
                    items = library,
 | 
			
		||||
                    selection = selectedManga,
 | 
			
		||||
                    onClick = onClickManga,
 | 
			
		||||
                    onLongClick = onLongClickManga,
 | 
			
		||||
                    searchQuery = searchQuery,
 | 
			
		||||
                    onGlobalSearchClicked = onGlobalSearchClicked,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            DisplayModeSetting.COMPACT_GRID -> {
 | 
			
		||||
                LibraryCompactGrid(
 | 
			
		||||
                    items = library,
 | 
			
		||||
                    columns = columns,
 | 
			
		||||
                    selection = selectedManga,
 | 
			
		||||
                    onClick = onClickManga,
 | 
			
		||||
                    onLongClick = onLongClickManga,
 | 
			
		||||
                    searchQuery = searchQuery,
 | 
			
		||||
                    onGlobalSearchClicked = onGlobalSearchClicked,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            DisplayModeSetting.COMFORTABLE_GRID -> {
 | 
			
		||||
                LibraryComfortableGrid(
 | 
			
		||||
                    items = library,
 | 
			
		||||
                    columns = columns,
 | 
			
		||||
                    selection = selectedManga,
 | 
			
		||||
                    onClick = onClickManga,
 | 
			
		||||
                    onLongClick = onLongClickManga,
 | 
			
		||||
                    searchQuery = searchQuery,
 | 
			
		||||
                    onGlobalSearchClicked = onGlobalSearchClicked,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            DisplayModeSetting.COVER_ONLY_GRID -> {
 | 
			
		||||
                LibraryCoverOnlyGrid(
 | 
			
		||||
                    items = library,
 | 
			
		||||
                    columns = columns,
 | 
			
		||||
                    selection = selectedManga,
 | 
			
		||||
                    onClick = onClickManga,
 | 
			
		||||
                    onLongClick = onLongClickManga,
 | 
			
		||||
                    searchQuery = searchQuery,
 | 
			
		||||
                    onGlobalSearchClicked = onGlobalSearchClicked,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.isSystemInDarkTheme
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.ScrollableTabRow
 | 
			
		||||
import androidx.compose.material3.Tab
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.unit.sp
 | 
			
		||||
import com.google.accompanist.pager.PagerState
 | 
			
		||||
import eu.kanade.domain.category.model.Category
 | 
			
		||||
import eu.kanade.presentation.components.DownloadedOnlyModeBanner
 | 
			
		||||
import eu.kanade.presentation.components.IncognitoModeBanner
 | 
			
		||||
import eu.kanade.presentation.components.Pill
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryTabs(
 | 
			
		||||
    state: PagerState,
 | 
			
		||||
    categories: List<Category>,
 | 
			
		||||
    showMangaCount: Boolean,
 | 
			
		||||
    isDownloadOnly: Boolean,
 | 
			
		||||
    isIncognitoMode: Boolean,
 | 
			
		||||
    getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
 | 
			
		||||
) {
 | 
			
		||||
    val scope = rememberCoroutineScope()
 | 
			
		||||
 | 
			
		||||
    val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
 | 
			
		||||
 | 
			
		||||
    Column {
 | 
			
		||||
        ScrollableTabRow(
 | 
			
		||||
            selectedTabIndex = state.currentPage,
 | 
			
		||||
            edgePadding = 0.dp,
 | 
			
		||||
        ) {
 | 
			
		||||
            categories.forEachIndexed { index, category ->
 | 
			
		||||
                val count by if (showMangaCount) {
 | 
			
		||||
                    getNumberOfMangaForCategory(category.id)
 | 
			
		||||
                } else {
 | 
			
		||||
                    remember { mutableStateOf<Int?>(null) }
 | 
			
		||||
                }
 | 
			
		||||
                Tab(
 | 
			
		||||
                    selected = state.currentPage == index,
 | 
			
		||||
                    onClick = { scope.launch { state.animateScrollToPage(index) } },
 | 
			
		||||
                    text = {
 | 
			
		||||
                        Row(
 | 
			
		||||
                            verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Text(text = category.name)
 | 
			
		||||
                            if (count != null) {
 | 
			
		||||
                                Pill(
 | 
			
		||||
                                    text = "$count",
 | 
			
		||||
                                    color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
 | 
			
		||||
                                    fontSize = 10.sp,
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (isDownloadOnly) {
 | 
			
		||||
            DownloadedOnlyModeBanner()
 | 
			
		||||
        }
 | 
			
		||||
        if (isIncognitoMode) {
 | 
			
		||||
            IncognitoModeBanner()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,188 @@
 | 
			
		||||
package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.isSystemInDarkTheme
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.statusBarsPadding
 | 
			
		||||
import androidx.compose.foundation.text.BasicTextField
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.outlined.ArrowBack
 | 
			
		||||
import androidx.compose.material.icons.outlined.Close
 | 
			
		||||
import androidx.compose.material.icons.outlined.FilterList
 | 
			
		||||
import androidx.compose.material.icons.outlined.FlipToBack
 | 
			
		||||
import androidx.compose.material.icons.outlined.Refresh
 | 
			
		||||
import androidx.compose.material.icons.outlined.Search
 | 
			
		||||
import androidx.compose.material.icons.outlined.SelectAll
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.IconButton
 | 
			
		||||
import androidx.compose.material3.LocalContentColor
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.SmallTopAppBar
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TopAppBarDefaults
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.drawBehind
 | 
			
		||||
import androidx.compose.ui.focus.FocusRequester
 | 
			
		||||
import androidx.compose.ui.focus.focusRequester
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.SolidColor
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.sp
 | 
			
		||||
import eu.kanade.presentation.components.Pill
 | 
			
		||||
import eu.kanade.presentation.library.LibraryState
 | 
			
		||||
import eu.kanade.presentation.theme.active
 | 
			
		||||
import kotlinx.coroutines.delay
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryToolbar(
 | 
			
		||||
    state: LibraryState,
 | 
			
		||||
    title: LibraryToolbarTitle,
 | 
			
		||||
    onClickUnselectAll: () -> Unit,
 | 
			
		||||
    onClickSelectAll: () -> Unit,
 | 
			
		||||
    onClickInvertSelection: () -> Unit,
 | 
			
		||||
    onClickFilter: () -> Unit,
 | 
			
		||||
    onClickRefresh: () -> Unit,
 | 
			
		||||
) = when {
 | 
			
		||||
    state.searchQuery != null -> LibrarySearchToolbar(
 | 
			
		||||
        searchQuery = state.searchQuery!!,
 | 
			
		||||
        onChangeSearchQuery = { state.searchQuery = it },
 | 
			
		||||
        onClickCloseSearch = { state.searchQuery = null },
 | 
			
		||||
    )
 | 
			
		||||
    state.selectionMode -> LibrarySelectionToolbar(
 | 
			
		||||
        state = state,
 | 
			
		||||
        onClickUnselectAll = onClickUnselectAll,
 | 
			
		||||
        onClickSelectAll = onClickSelectAll,
 | 
			
		||||
        onClickInvertSelection = onClickInvertSelection,
 | 
			
		||||
    )
 | 
			
		||||
    else -> LibraryRegularToolbar(
 | 
			
		||||
        title = title,
 | 
			
		||||
        hasFilters = state.hasActiveFilters,
 | 
			
		||||
        onClickSearch = { state.searchQuery = "" },
 | 
			
		||||
        onClickFilter = onClickFilter,
 | 
			
		||||
        onClickRefresh = onClickRefresh,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryRegularToolbar(
 | 
			
		||||
    title: LibraryToolbarTitle,
 | 
			
		||||
    hasFilters: Boolean,
 | 
			
		||||
    onClickSearch: () -> Unit,
 | 
			
		||||
    onClickFilter: () -> Unit,
 | 
			
		||||
    onClickRefresh: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
 | 
			
		||||
    val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
 | 
			
		||||
    SmallTopAppBar(
 | 
			
		||||
        modifier = Modifier.statusBarsPadding(),
 | 
			
		||||
        title = {
 | 
			
		||||
            Row(verticalAlignment = Alignment.CenterVertically) {
 | 
			
		||||
                Text(
 | 
			
		||||
                    text = title.text,
 | 
			
		||||
                    maxLines = 1,
 | 
			
		||||
                    modifier = Modifier.weight(1f, false),
 | 
			
		||||
                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                )
 | 
			
		||||
                if (title.numberOfManga != null) {
 | 
			
		||||
                    Pill(
 | 
			
		||||
                        text = "${title.numberOfManga}",
 | 
			
		||||
                        color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
 | 
			
		||||
                        fontSize = 14.sp,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        actions = {
 | 
			
		||||
            IconButton(onClick = onClickSearch) {
 | 
			
		||||
                Icon(Icons.Outlined.Search, contentDescription = "search")
 | 
			
		||||
            }
 | 
			
		||||
            IconButton(onClick = onClickFilter) {
 | 
			
		||||
                Icon(Icons.Outlined.FilterList, contentDescription = "search", tint = filterTint)
 | 
			
		||||
            }
 | 
			
		||||
            IconButton(onClick = onClickRefresh) {
 | 
			
		||||
                Icon(Icons.Outlined.Refresh, contentDescription = "search")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibrarySelectionToolbar(
 | 
			
		||||
    state: LibraryState,
 | 
			
		||||
    onClickUnselectAll: () -> Unit,
 | 
			
		||||
    onClickSelectAll: () -> Unit,
 | 
			
		||||
    onClickInvertSelection: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(1f)
 | 
			
		||||
    SmallTopAppBar(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .drawBehind {
 | 
			
		||||
                drawRect(backgroundColor.copy(alpha = 1f))
 | 
			
		||||
            }
 | 
			
		||||
            .statusBarsPadding(),
 | 
			
		||||
        navigationIcon = {
 | 
			
		||||
            IconButton(onClick = onClickUnselectAll) {
 | 
			
		||||
                Icon(Icons.Outlined.Close, contentDescription = "close")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        title = {
 | 
			
		||||
            Text(text = "${state.selection.size}")
 | 
			
		||||
        },
 | 
			
		||||
        actions = {
 | 
			
		||||
            IconButton(onClick = onClickSelectAll) {
 | 
			
		||||
                Icon(Icons.Outlined.SelectAll, contentDescription = "search")
 | 
			
		||||
            }
 | 
			
		||||
            IconButton(onClick = onClickInvertSelection) {
 | 
			
		||||
                Icon(Icons.Outlined.FlipToBack, contentDescription = "invert")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        colors = TopAppBarDefaults.smallTopAppBarColors(
 | 
			
		||||
            containerColor = Color.Transparent,
 | 
			
		||||
            scrolledContainerColor = Color.Transparent,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibrarySearchToolbar(
 | 
			
		||||
    searchQuery: String,
 | 
			
		||||
    onChangeSearchQuery: (String) -> Unit,
 | 
			
		||||
    onClickCloseSearch: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val focusRequester = remember { FocusRequester.Default }
 | 
			
		||||
    SmallTopAppBar(
 | 
			
		||||
        modifier = Modifier.statusBarsPadding(),
 | 
			
		||||
        navigationIcon = {
 | 
			
		||||
            IconButton(onClick = onClickCloseSearch) {
 | 
			
		||||
                Icon(Icons.Outlined.ArrowBack, contentDescription = "back")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        title = {
 | 
			
		||||
            BasicTextField(
 | 
			
		||||
                value = searchQuery,
 | 
			
		||||
                onValueChange = onChangeSearchQuery,
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .focusRequester(focusRequester),
 | 
			
		||||
                textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
 | 
			
		||||
                singleLine = true,
 | 
			
		||||
                cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
 | 
			
		||||
            )
 | 
			
		||||
            LaunchedEffect(focusRequester) {
 | 
			
		||||
                // TODO: https://issuetracker.google.com/issues/204502668
 | 
			
		||||
                delay(100)
 | 
			
		||||
                focusRequester.requestFocus()
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class LibraryToolbarTitle(
 | 
			
		||||
    val text: String,
 | 
			
		||||
    val numberOfManga: Int? = null,
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										12
									
								
								app/src/main/java/eu/kanade/presentation/theme/Color.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/src/main/java/eu/kanade/presentation/theme/Color.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package eu.kanade.presentation.theme
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.isSystemInDarkTheme
 | 
			
		||||
import androidx.compose.material3.ColorScheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
 | 
			
		||||
val ColorScheme.active: Color
 | 
			
		||||
    @Composable
 | 
			
		||||
    get() {
 | 
			
		||||
        return if (isSystemInDarkTheme()) Color(255, 235, 59) else Color(255, 193, 7)
 | 
			
		||||
    }
 | 
			
		||||
@@ -1,208 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
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.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.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.ComposeControllerBinding
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.setComposeContent
 | 
			
		||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This adapter stores the categories from the library, used with a ViewPager.
 | 
			
		||||
 *
 | 
			
		||||
 * @constructor creates an instance of the adapter.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryAdapter(
 | 
			
		||||
    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> = mutableStateListOf()
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The number of manga in each category.
 | 
			
		||||
     * List order must be the same as [categories]
 | 
			
		||||
     */
 | 
			
		||||
    private var itemsPerCategory: List<Int> = emptyList()
 | 
			
		||||
 | 
			
		||||
    private var boundViews = arrayListOf<View>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pair of category and size of category
 | 
			
		||||
     */
 | 
			
		||||
    fun updateCategories(new: List<Pair<Category, Int>>) {
 | 
			
		||||
        var updated = false
 | 
			
		||||
 | 
			
		||||
        val newCategories = new.map { it.first }
 | 
			
		||||
        if (categories != newCategories) {
 | 
			
		||||
            categories = newCategories
 | 
			
		||||
            updated = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val newItemsPerCategory = new.map { it.second }
 | 
			
		||||
        if (itemsPerCategory !== newItemsPerCategory) {
 | 
			
		||||
            itemsPerCategory = newItemsPerCategory
 | 
			
		||||
            updated = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (updated) {
 | 
			
		||||
            notifyDataSetChanged()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view for this adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @return a new view.
 | 
			
		||||
     */
 | 
			
		||||
    override fun inflateView(container: ViewGroup, viewType: Int): View {
 | 
			
		||||
        val binding = ComposeControllerBinding.inflate(LayoutInflater.from(container.context), container, false)
 | 
			
		||||
        return binding.root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds a view with a position.
 | 
			
		||||
     *
 | 
			
		||||
     * @param view the view to bind.
 | 
			
		||||
     * @param position the position in the adapter.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindView(view: View, position: Int) {
 | 
			
		||||
        (view as ComposeView).apply {
 | 
			
		||||
            setComposeContent {
 | 
			
		||||
                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,
 | 
			
		||||
                                selection = presenter.selection,
 | 
			
		||||
                                onClick = onClickManga,
 | 
			
		||||
                                onLongClick = onLongClickManga,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Recycles a view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param view the view to recycle.
 | 
			
		||||
     * @param position the position in the adapter.
 | 
			
		||||
     */
 | 
			
		||||
    override fun recycleView(view: View, position: Int) {
 | 
			
		||||
        boundViews.remove(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the number of categories.
 | 
			
		||||
     *
 | 
			
		||||
     * @return the number of categories or 0 if the list is null.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getCount(): Int {
 | 
			
		||||
        return categories.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the title to display for a category.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element.
 | 
			
		||||
     * @return the title to display.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getPageTitle(position: Int): CharSequence {
 | 
			
		||||
        return if (!preferences.categoryNumberOfItems().get()) {
 | 
			
		||||
            categories[position].name
 | 
			
		||||
        } else {
 | 
			
		||||
            categories[position].let { "${it.name} (${itemsPerCategory[position]})" }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getViewType(position: Int): Int = -1
 | 
			
		||||
}
 | 
			
		||||
@@ -1,240 +1,119 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuInflater
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.appcompat.view.ActionMode
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
import com.fredporciuncula.flow.preferences.Preference
 | 
			
		||||
import com.google.android.material.tabs.TabLayout
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
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.presentation.library.LibraryScreen
 | 
			
		||||
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.ui.base.controller.FullComposeController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
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.asHotFlow
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getResourceColor
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.openInBrowser
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
 | 
			
		||||
import eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
 | 
			
		||||
import kotlinx.coroutines.cancel
 | 
			
		||||
import kotlinx.coroutines.flow.drop
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import reactivecircus.flowbinding.android.view.clicks
 | 
			
		||||
import reactivecircus.flowbinding.viewpager.pageSelections
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class LibraryController(
 | 
			
		||||
    bundle: Bundle? = null,
 | 
			
		||||
    private val preferences: PreferencesHelper = Injekt.get(),
 | 
			
		||||
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
 | 
			
		||||
) : FullComposeController<LibraryPresenter>(bundle),
 | 
			
		||||
    RootController,
 | 
			
		||||
    TabbedController,
 | 
			
		||||
    ActionModeWithToolbar.Callback,
 | 
			
		||||
    ChangeMangaCategoriesDialog.Listener,
 | 
			
		||||
    DeleteLibraryMangasDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Position of the active category.
 | 
			
		||||
     */
 | 
			
		||||
    private var activeCategory: Int = preferences.lastUsedCategory().get()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Action mode for selections.
 | 
			
		||||
     */
 | 
			
		||||
    private var actionMode: ActionModeWithToolbar? = null
 | 
			
		||||
 | 
			
		||||
    private var mangaMap: LibraryMap = emptyMap()
 | 
			
		||||
 | 
			
		||||
    private var adapter: LibraryAdapter? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sheet containing filter/sort/display items.
 | 
			
		||||
     */
 | 
			
		||||
    private var settingsSheet: LibrarySettingsSheet? = null
 | 
			
		||||
 | 
			
		||||
    private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
 | 
			
		||||
 | 
			
		||||
    private var mangaCountVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
 | 
			
		||||
 | 
			
		||||
    private var tabsVisibilitySubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    private var mangaCountVisibilitySubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
        retainViewMode = RetainViewMode.RETAIN_DETACH
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var currentTitle: String? = null
 | 
			
		||||
        set(value) {
 | 
			
		||||
            if (field != value) {
 | 
			
		||||
                field = value
 | 
			
		||||
                setTitle()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    override fun createPresenter(): LibraryPresenter = LibraryPresenter()
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return currentTitle ?: resources?.getString(R.string.label_library)
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val context = LocalContext.current
 | 
			
		||||
        LibraryScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            onMangaClicked = ::openManga,
 | 
			
		||||
            onGlobalSearchClicked = {
 | 
			
		||||
                router.pushController(GlobalSearchController(presenter.query))
 | 
			
		||||
            },
 | 
			
		||||
            onChangeCategoryClicked = ::showMangaCategoriesDialog,
 | 
			
		||||
            onMarkAsReadClicked = { markReadStatus(true) },
 | 
			
		||||
            onMarkAsUnreadClicked = { markReadStatus(false) },
 | 
			
		||||
            onDownloadClicked = ::downloadUnreadChapters,
 | 
			
		||||
            onDeleteClicked = ::showDeleteMangaDialog,
 | 
			
		||||
            onClickFilter = ::showSettingsSheet,
 | 
			
		||||
            onClickRefresh = {
 | 
			
		||||
                if (LibraryUpdateService.start(context)) {
 | 
			
		||||
                    context.toast(R.string.updating_library)
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            onClickInvertSelection = { presenter.invertSelection(presenter.activeCategory) },
 | 
			
		||||
            onClickSelectAll = { presenter.selectAll(presenter.activeCategory) },
 | 
			
		||||
            onClickUnselectAll = ::clearSelection,
 | 
			
		||||
        )
 | 
			
		||||
        LaunchedEffect(presenter.selectionMode) {
 | 
			
		||||
            val activity = (activity as? MainActivity) ?: return@LaunchedEffect
 | 
			
		||||
            activity.showBottomNav(presenter.selectionMode.not())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateTitle() {
 | 
			
		||||
        val showCategoryTabs = preferences.categoryTabs().get()
 | 
			
		||||
        val currentCategory = adapter?.categories?.getOrNull(binding.libraryPager.currentItem)
 | 
			
		||||
 | 
			
		||||
        var title = if (showCategoryTabs) {
 | 
			
		||||
            resources?.getString(R.string.label_library)
 | 
			
		||||
        } else {
 | 
			
		||||
            currentCategory?.name
 | 
			
		||||
    override fun handleBack(): Boolean {
 | 
			
		||||
        if (presenter.selection.isNotEmpty()) {
 | 
			
		||||
            presenter.clearSelection()
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (preferences.categoryNumberOfItems().get()) {
 | 
			
		||||
            if (!showCategoryTabs || adapter?.categories?.size == 1) {
 | 
			
		||||
                title += " (${mangaMap[currentCategory?.id]?.size ?: 0})"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        currentTitle = title
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): LibraryPresenter {
 | 
			
		||||
        return LibraryPresenter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        adapter = LibraryAdapter(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            onClickManga = {
 | 
			
		||||
                openManga(it.id!!)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
            .asHotFlow { presenter.columns = it }
 | 
			
		||||
            .launchIn(viewScope)
 | 
			
		||||
 | 
			
		||||
        binding.libraryPager.adapter = adapter
 | 
			
		||||
        binding.libraryPager.pageSelections()
 | 
			
		||||
            .drop(1)
 | 
			
		||||
            .onEach {
 | 
			
		||||
                preferences.lastUsedCategory().set(it)
 | 
			
		||||
                activeCategory = it
 | 
			
		||||
                updateTitle()
 | 
			
		||||
            }
 | 
			
		||||
            .launchIn(viewScope)
 | 
			
		||||
 | 
			
		||||
        if (adapter!!.categories.isNotEmpty()) {
 | 
			
		||||
            createActionModeIfNeeded()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        settingsSheet = LibrarySettingsSheet(router) { group ->
 | 
			
		||||
            when (group) {
 | 
			
		||||
                is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
 | 
			
		||||
                is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
 | 
			
		||||
                is LibrarySettingsSheet.Display.DisplayGroup -> {
 | 
			
		||||
                    val delay = if (preferences.categorizedDisplaySettings().get()) 125L else 0L
 | 
			
		||||
 | 
			
		||||
                    Observable.timer(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
 | 
			
		||||
                        .subscribe {
 | 
			
		||||
                            reattachAdapter()
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                is LibrarySettingsSheet.Display.DisplayGroup -> {}
 | 
			
		||||
                is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
 | 
			
		||||
                is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
 | 
			
		||||
                is LibrarySettingsSheet.Display.TabsGroup -> {} // onTabsSettingsChanged()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.btnGlobalSearch.clicks()
 | 
			
		||||
            .onEach {
 | 
			
		||||
                router.pushController(GlobalSearchController(presenter.query))
 | 
			
		||||
            }
 | 
			
		||||
            .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) {
 | 
			
		||||
            (activity as? MainActivity)?.binding?.tabs?.setupWithViewPager(binding.libraryPager)
 | 
			
		||||
            presenter.subscribeLibrary()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
        adapter = null
 | 
			
		||||
        settingsSheet?.sheetScope?.cancel()
 | 
			
		||||
        settingsSheet = null
 | 
			
		||||
        tabsVisibilitySubscription?.unsubscribe()
 | 
			
		||||
        tabsVisibilitySubscription = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun configureTabs(tabs: TabLayout): Boolean {
 | 
			
		||||
        with(tabs) {
 | 
			
		||||
            isVisible = false
 | 
			
		||||
            tabGravity = TabLayout.GRAVITY_START
 | 
			
		||||
            tabMode = TabLayout.MODE_SCROLLABLE
 | 
			
		||||
        }
 | 
			
		||||
        tabsVisibilitySubscription?.unsubscribe()
 | 
			
		||||
        tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible ->
 | 
			
		||||
            tabs.isVisible = visible
 | 
			
		||||
        }
 | 
			
		||||
        mangaCountVisibilitySubscription?.unsubscribe()
 | 
			
		||||
        mangaCountVisibilitySubscription = mangaCountVisibilityRelay.subscribe {
 | 
			
		||||
            adapter?.notifyDataSetChanged()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupTabs(tabs: TabLayout) {
 | 
			
		||||
        tabsVisibilitySubscription?.unsubscribe()
 | 
			
		||||
        tabsVisibilitySubscription = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun showSettingsSheet() {
 | 
			
		||||
        if (adapter?.categories?.isNotEmpty() == true) {
 | 
			
		||||
            adapter?.categories?.get(binding.libraryPager.currentItem)?.let { category ->
 | 
			
		||||
        if (presenter.categories.isNotEmpty()) {
 | 
			
		||||
            presenter.categories[presenter.activeCategory].let { category ->
 | 
			
		||||
                settingsSheet?.show(category.toDbCategory())
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -242,61 +121,6 @@ class LibraryController(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onNextLibraryUpdate(categories: List<Category>, mangaMap: LibraryMap) {
 | 
			
		||||
        val view = view ?: return
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
 | 
			
		||||
        // Show empty view if needed
 | 
			
		||||
        if (mangaMap.isNotEmpty()) {
 | 
			
		||||
            binding.emptyView.hide()
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.emptyView.show(
 | 
			
		||||
                R.string.information_empty_library,
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) {
 | 
			
		||||
                        activity?.openInBrowser("https://tachiyomi.org/help/guides/getting-started")
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
            (activity as? MainActivity)?.ready = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the current active category.
 | 
			
		||||
        val activeCat = if (adapter.categories.isNotEmpty()) {
 | 
			
		||||
            binding.libraryPager.currentItem
 | 
			
		||||
        } else {
 | 
			
		||||
            activeCategory
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set the categories
 | 
			
		||||
        adapter.updateCategories(categories.map { it to (mangaMap[it.id]?.size ?: 0) })
 | 
			
		||||
 | 
			
		||||
        // Restore active category.
 | 
			
		||||
        binding.libraryPager.setCurrentItem(activeCat, false)
 | 
			
		||||
 | 
			
		||||
        // Trigger display of tabs
 | 
			
		||||
        onTabsSettingsChanged(firstLaunch = true)
 | 
			
		||||
 | 
			
		||||
        // Delay the scroll position to allow the view to be properly measured.
 | 
			
		||||
        view.post {
 | 
			
		||||
            if (isAttached) {
 | 
			
		||||
                (activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
        this.mangaMap = mangaMap
 | 
			
		||||
 | 
			
		||||
        // Finally update the title
 | 
			
		||||
        updateTitle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onFilterChanged() {
 | 
			
		||||
        presenter.requestFilterUpdate()
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
@@ -306,146 +130,17 @@ class LibraryController(
 | 
			
		||||
        presenter.requestBadgesUpdate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onTabsSettingsChanged(firstLaunch: Boolean = false) {
 | 
			
		||||
        if (!firstLaunch) {
 | 
			
		||||
            mangaCountVisibilityRelay.call(preferences.categoryNumberOfItems().get())
 | 
			
		||||
        }
 | 
			
		||||
        tabsVisibilityRelay.call(preferences.categoryTabs().get() && (adapter?.categories?.size ?: 0) > 1)
 | 
			
		||||
        updateTitle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onSortChanged() {
 | 
			
		||||
        presenter.requestSortUpdate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reattaches the adapter to the view pager to recreate fragments
 | 
			
		||||
     */
 | 
			
		||||
    private fun reattachAdapter() {
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
 | 
			
		||||
        val position = binding.libraryPager.currentItem
 | 
			
		||||
 | 
			
		||||
        adapter.recycle = false
 | 
			
		||||
        binding.libraryPager.adapter = adapter
 | 
			
		||||
        binding.libraryPager.currentItem = position
 | 
			
		||||
        adapter.recycle = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createActionModeIfNeeded() {
 | 
			
		||||
        val activity = activity
 | 
			
		||||
        if (actionMode == null && activity is MainActivity) {
 | 
			
		||||
            actionMode = activity.startActionModeAndToolbar(this)
 | 
			
		||||
            activity.showBottomNav(false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun destroyActionModeIfNeeded() {
 | 
			
		||||
        actionMode?.finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
        createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
 | 
			
		||||
        // Mutate the filter icon because it needs to be tinted and the resource is shared.
 | 
			
		||||
        menu.findItem(R.id.action_filter).icon?.mutate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(query: String) {
 | 
			
		||||
        presenter.query = query
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun performSearch() {
 | 
			
		||||
        if (presenter.query.isNotEmpty()) {
 | 
			
		||||
            binding.btnGlobalSearch.isVisible = true
 | 
			
		||||
            binding.btnGlobalSearch.text =
 | 
			
		||||
                resources?.getString(R.string.action_global_search_query, presenter.query)
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.btnGlobalSearch.isVisible = false
 | 
			
		||||
        }
 | 
			
		||||
        presenter.searchQuery = query
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        val settingsSheet = settingsSheet ?: return
 | 
			
		||||
 | 
			
		||||
        val filterItem = menu.findItem(R.id.action_filter)
 | 
			
		||||
 | 
			
		||||
        // Tint icon if there's a filter active
 | 
			
		||||
        if (settingsSheet.filters.hasActiveFilters()) {
 | 
			
		||||
            val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
 | 
			
		||||
            filterItem.icon?.setTint(filterColor)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_search -> expandActionViewFromInteraction = true
 | 
			
		||||
            R.id.action_filter -> showSettingsSheet()
 | 
			
		||||
            R.id.action_update_library -> {
 | 
			
		||||
                activity?.let {
 | 
			
		||||
                    if (LibraryUpdateService.start(it)) {
 | 
			
		||||
                        it.toast(R.string.updating_library)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return super.onOptionsItemSelected(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates the action mode, forcing it to refresh its content.
 | 
			
		||||
     */
 | 
			
		||||
    fun invalidateActionMode() {
 | 
			
		||||
        actionMode?.invalidate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
 | 
			
		||||
        mode.menuInflater.inflate(R.menu.generic_selection, menu)
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
 | 
			
		||||
        menuInflater.inflate(R.menu.library_selection, menu)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
 | 
			
		||||
        val count = presenter.selection.size
 | 
			
		||||
        if (count == 0) {
 | 
			
		||||
            // Destroy action mode if there are no items selected.
 | 
			
		||||
            destroyActionModeIfNeeded()
 | 
			
		||||
        } else {
 | 
			
		||||
            mode.title = count.toString()
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
 | 
			
		||||
        if (presenter.hasSelection().not()) return
 | 
			
		||||
        toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible =
 | 
			
		||||
            presenter.selection.any { presenter.loadedManga.values.any { it.any { it.isLocal } } }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_move_to_category -> showMangaCategoriesDialog()
 | 
			
		||||
            R.id.action_download_unread -> downloadUnreadChapters()
 | 
			
		||||
            R.id.action_mark_as_read -> markReadStatus(true)
 | 
			
		||||
            R.id.action_mark_as_unread -> markReadStatus(false)
 | 
			
		||||
            R.id.action_delete -> showDeleteMangaDialog()
 | 
			
		||||
            R.id.action_select_all -> selectAllCategoryManga()
 | 
			
		||||
            R.id.action_select_inverse -> selectInverseCategoryManga()
 | 
			
		||||
            else -> return false
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyActionMode(mode: ActionMode) {
 | 
			
		||||
        // Clear all the manga selections and notify child views.
 | 
			
		||||
        presenter.clearSelection()
 | 
			
		||||
 | 
			
		||||
        (activity as? MainActivity)?.showBottomNav(true)
 | 
			
		||||
 | 
			
		||||
        actionMode = null
 | 
			
		||||
        presenter.hasActiveFilters = settingsSheet.filters.hasActiveFilters()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openManga(mangaId: Long) {
 | 
			
		||||
@@ -461,7 +156,6 @@ class LibraryController(
 | 
			
		||||
     */
 | 
			
		||||
    fun clearSelection() {
 | 
			
		||||
        presenter.clearSelection()
 | 
			
		||||
        invalidateActionMode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -496,13 +190,13 @@ class LibraryController(
 | 
			
		||||
    private fun downloadUnreadChapters() {
 | 
			
		||||
        val mangas = presenter.selection.toList()
 | 
			
		||||
        presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() })
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
        presenter.clearSelection()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun markReadStatus(read: Boolean) {
 | 
			
		||||
        val mangas = presenter.selection.toList()
 | 
			
		||||
        presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read)
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
        presenter.clearSelection()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun showDeleteMangaDialog() {
 | 
			
		||||
@@ -512,28 +206,11 @@ class LibraryController(
 | 
			
		||||
 | 
			
		||||
    override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
 | 
			
		||||
        presenter.setMangaCategories(mangas, addCategories, removeCategories)
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
        presenter.clearSelection()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun deleteMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
 | 
			
		||||
        presenter.removeMangas(mangas.map { it.toDbManga() }, deleteFromLibrary, deleteChapters)
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun selectAllCategoryManga() {
 | 
			
		||||
        presenter.selectAll(binding.libraryPager.currentItem)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun selectInverseCategoryManga() {
 | 
			
		||||
        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()
 | 
			
		||||
        }
 | 
			
		||||
        presenter.clearSelection()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,15 @@ 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.produceState
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.util.fastAny
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import eu.kanade.core.prefs.PreferenceMutableState
 | 
			
		||||
import eu.kanade.core.util.asFlow
 | 
			
		||||
import eu.kanade.core.util.asObservable
 | 
			
		||||
import eu.kanade.data.DatabaseHandler
 | 
			
		||||
import eu.kanade.domain.category.interactor.GetCategories
 | 
			
		||||
@@ -25,6 +27,10 @@ import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.domain.manga.model.MangaUpdate
 | 
			
		||||
import eu.kanade.domain.manga.model.isLocal
 | 
			
		||||
import eu.kanade.domain.track.interactor.GetTracks
 | 
			
		||||
import eu.kanade.presentation.library.LibraryState
 | 
			
		||||
import eu.kanade.presentation.library.LibraryStateImpl
 | 
			
		||||
import eu.kanade.presentation.library.components.LibraryToolbarTitle
 | 
			
		||||
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
 | 
			
		||||
@@ -39,14 +45,16 @@ 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.removeCovers
 | 
			
		||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
@@ -70,6 +78,7 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
 | 
			
		||||
 * Presenter of [LibraryController].
 | 
			
		||||
 */
 | 
			
		||||
class LibraryPresenter(
 | 
			
		||||
    private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
 | 
			
		||||
    private val handler: DatabaseHandler = Injekt.get(),
 | 
			
		||||
    private val getLibraryManga: GetLibraryManga = Injekt.get(),
 | 
			
		||||
    private val getTracks: GetTracks = Injekt.get(),
 | 
			
		||||
@@ -83,31 +92,27 @@ class LibraryPresenter(
 | 
			
		||||
    private val sourceManager: SourceManager = Injekt.get(),
 | 
			
		||||
    private val downloadManager: DownloadManager = Injekt.get(),
 | 
			
		||||
    private val trackManager: TrackManager = Injekt.get(),
 | 
			
		||||
) : BasePresenter<LibraryController>() {
 | 
			
		||||
) : BasePresenter<LibraryController>(), LibraryState by state {
 | 
			
		||||
 | 
			
		||||
    private val context = preferences.context
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Categories of the library.
 | 
			
		||||
     */
 | 
			
		||||
    var categories: List<Category> = mutableStateListOf()
 | 
			
		||||
    var loadedManga by mutableStateOf(emptyMap<Long, List<LibraryItem>>())
 | 
			
		||||
        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 preferences.categorizedDisplaySettings().asState()
 | 
			
		||||
 | 
			
		||||
    var columns by mutableStateOf(0)
 | 
			
		||||
 | 
			
		||||
    var currentDisplayMode by preferences.libraryDisplayMode().asState()
 | 
			
		||||
 | 
			
		||||
    val tabVisibility by preferences.categoryTabs().asState()
 | 
			
		||||
 | 
			
		||||
    val mangaCountVisibility by preferences.categoryNumberOfItems().asState()
 | 
			
		||||
 | 
			
		||||
    var activeCategory: Int by preferences.lastUsedCategory().asState()
 | 
			
		||||
 | 
			
		||||
    val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
 | 
			
		||||
 | 
			
		||||
    val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Relay used to apply the UI filters to the last emission of the library.
 | 
			
		||||
     */
 | 
			
		||||
@@ -123,7 +128,7 @@ class LibraryPresenter(
 | 
			
		||||
     */
 | 
			
		||||
    private val sortTriggerRelay = BehaviorRelay.create(Unit)
 | 
			
		||||
 | 
			
		||||
    private var librarySubscription: Subscription? = null
 | 
			
		||||
    private var librarySubscription: Job? = null
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
@@ -135,22 +140,31 @@ class LibraryPresenter(
 | 
			
		||||
     * Subscribes to library if needed.
 | 
			
		||||
     */
 | 
			
		||||
    fun subscribeLibrary() {
 | 
			
		||||
        // TODO: Move this to a coroutine world
 | 
			
		||||
        if (librarySubscription.isNullOrUnsubscribed()) {
 | 
			
		||||
            librarySubscription = getLibraryObservable()
 | 
			
		||||
                .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
 | 
			
		||||
                    lib.apply { setBadges(mangaMap) }
 | 
			
		||||
                }
 | 
			
		||||
                .combineLatest(getFilterObservable()) { lib, tracks ->
 | 
			
		||||
                    lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks))
 | 
			
		||||
                }
 | 
			
		||||
                .combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
 | 
			
		||||
                    lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
 | 
			
		||||
                }
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeLatestCache({ view, (categories, mangaMap) ->
 | 
			
		||||
                    view.onNextLibraryUpdate(categories, mangaMap)
 | 
			
		||||
                },)
 | 
			
		||||
        /**
 | 
			
		||||
         * TODO: Move this to a coroutine world
 | 
			
		||||
         * - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library
 | 
			
		||||
         * - Create new db view and new query to just fetch the current category save as needed to instance variable
 | 
			
		||||
         * - Fetch badges to maps and retrive as needed instead of fetching all of them at once
 | 
			
		||||
         */
 | 
			
		||||
        if (librarySubscription == null || librarySubscription!!.isCancelled) {
 | 
			
		||||
            librarySubscription = presenterScope.launchIO {
 | 
			
		||||
                getLibraryObservable()
 | 
			
		||||
                    .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
 | 
			
		||||
                        lib.apply { setBadges(mangaMap) }
 | 
			
		||||
                    }
 | 
			
		||||
                    .combineLatest(getFilterObservable()) { lib, tracks ->
 | 
			
		||||
                        lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks))
 | 
			
		||||
                    }
 | 
			
		||||
                    .combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
 | 
			
		||||
                        lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
 | 
			
		||||
                    }
 | 
			
		||||
                    .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                    .asFlow()
 | 
			
		||||
                    .collectLatest {
 | 
			
		||||
                        state.isLoading = false
 | 
			
		||||
                        loadedManga = it.mangaMap
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -397,7 +411,7 @@ class LibraryPresenter(
 | 
			
		||||
     * @return an observable of the categories and its manga.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getLibraryObservable(): Observable<Library> {
 | 
			
		||||
        return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga ->
 | 
			
		||||
        return combine(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga ->
 | 
			
		||||
            val categories = if (libraryManga.containsKey(0)) {
 | 
			
		||||
                arrayListOf(Category.default(context)) + dbCategories
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -411,9 +425,9 @@ class LibraryPresenter(
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.categories = categories
 | 
			
		||||
            state.categories = categories
 | 
			
		||||
            Library(categories, libraryManga)
 | 
			
		||||
        }
 | 
			
		||||
        }.asObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -421,8 +435,8 @@ class LibraryPresenter(
 | 
			
		||||
     *
 | 
			
		||||
     * @return an observable of the categories.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getCategoriesObservable(): Observable<List<Category>> {
 | 
			
		||||
        return getCategories.subscribe().asObservable()
 | 
			
		||||
    private fun getCategoriesObservable(): Flow<List<Category>> {
 | 
			
		||||
        return getCategories.subscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -431,8 +445,8 @@ class LibraryPresenter(
 | 
			
		||||
     * @return an observable containing a map with the category id as key and a list of manga as the
 | 
			
		||||
     * value.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getLibraryMangasObservable(): Observable<LibraryMap> {
 | 
			
		||||
        return getLibraryManga.subscribe().asObservable()
 | 
			
		||||
    private fun getLibraryMangasObservable(): Flow<LibraryMap> {
 | 
			
		||||
        return getLibraryManga.subscribe()
 | 
			
		||||
            .map { list ->
 | 
			
		||||
                list.map { libraryManga ->
 | 
			
		||||
                    // Display mode based on user preference: take it from global library setting or category
 | 
			
		||||
@@ -447,7 +461,8 @@ class LibraryPresenter(
 | 
			
		||||
     * @return an observable of tracked manga.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
 | 
			
		||||
        return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks }
 | 
			
		||||
        return filterTriggerRelay.observeOn(Schedulers.io())
 | 
			
		||||
            .combineLatest(getTracksObservable()) { _, tracks -> tracks }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -458,7 +473,7 @@ class LibraryPresenter(
 | 
			
		||||
    private fun getTracksObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
 | 
			
		||||
        // TODO: Move this to domain/data layer
 | 
			
		||||
        return getTracks.subscribe()
 | 
			
		||||
            .asObservable().map { tracks ->
 | 
			
		||||
            .map { tracks ->
 | 
			
		||||
                tracks
 | 
			
		||||
                    .groupBy { it.mangaId }
 | 
			
		||||
                    .mapValues { tracksForMangaId ->
 | 
			
		||||
@@ -468,6 +483,7 @@ class LibraryPresenter(
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
            .asObservable()
 | 
			
		||||
            .observeOn(Schedulers.io())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -497,7 +513,7 @@ class LibraryPresenter(
 | 
			
		||||
     */
 | 
			
		||||
    fun onOpenManga() {
 | 
			
		||||
        // Avoid further db updates for the library when it's not needed
 | 
			
		||||
        librarySubscription?.let { remove(it) }
 | 
			
		||||
        librarySubscription?.cancel()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -610,14 +626,50 @@ class LibraryPresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State<List<LibraryItem>> {
 | 
			
		||||
    fun getMangaCountForCategory(categoryId: Long): androidx.compose.runtime.State<Int?> {
 | 
			
		||||
        return produceState<Int?>(initialValue = null, loadedManga) {
 | 
			
		||||
            value = loadedManga[categoryId]?.size
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
 | 
			
		||||
        return (if (isLandscape) preferences.landscapeColumns() else preferences.portraitColumns()).asState()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: This is good but should we separate title from count or get categories with count from db
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getToolbarTitle(): androidx.compose.runtime.State<LibraryToolbarTitle> {
 | 
			
		||||
        val category = categories.getOrNull(activeCategory)
 | 
			
		||||
 | 
			
		||||
        val defaultTitle = stringResource(id = R.string.label_library)
 | 
			
		||||
        val default = remember { LibraryToolbarTitle(defaultTitle) }
 | 
			
		||||
 | 
			
		||||
        return produceState(initialValue = default, category, mangaCountVisibility, tabVisibility) {
 | 
			
		||||
            val title = if (tabVisibility.not()) category?.name ?: defaultTitle else defaultTitle
 | 
			
		||||
 | 
			
		||||
            value = when {
 | 
			
		||||
                category == null -> default
 | 
			
		||||
                (tabVisibility.not() && mangaCountVisibility.not()) -> LibraryToolbarTitle(title)
 | 
			
		||||
                tabVisibility.not() && mangaCountVisibility -> LibraryToolbarTitle(title, loadedManga[category.id]?.size)
 | 
			
		||||
                (tabVisibility && categories.size > 1) && mangaCountVisibility -> LibraryToolbarTitle(title)
 | 
			
		||||
                tabVisibility && mangaCountVisibility -> LibraryToolbarTitle(title, loadedManga[category.id]?.size)
 | 
			
		||||
                else -> default
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getMangaForCategory(page: Int): androidx.compose.runtime.State<List<LibraryItem>> {
 | 
			
		||||
        val categoryId = remember(categories) {
 | 
			
		||||
            categories.getOrNull(page)?.id ?: -1
 | 
			
		||||
        }
 | 
			
		||||
        val unfiltered = loadedManga[categoryId] ?: emptyList()
 | 
			
		||||
 | 
			
		||||
        return derivedStateOf {
 | 
			
		||||
            val query = searchQuery
 | 
			
		||||
            if (query.isNotBlank()) {
 | 
			
		||||
            if (query.isNullOrBlank().not()) {
 | 
			
		||||
                unfiltered.filter {
 | 
			
		||||
                    it.filter(query)
 | 
			
		||||
                    it.filter(query!!)
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                unfiltered
 | 
			
		||||
@@ -626,9 +678,9 @@ class LibraryPresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getDisplayMode(index: Int): DisplayModeSetting {
 | 
			
		||||
    fun getDisplayMode(index: Int): androidx.compose.runtime.State<DisplayModeSetting> {
 | 
			
		||||
        val category = categories[index]
 | 
			
		||||
        return remember {
 | 
			
		||||
        return derivedStateOf {
 | 
			
		||||
            if (isPerCategory.not() || category.id == 0L) {
 | 
			
		||||
                currentDisplayMode
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -642,34 +694,30 @@ class LibraryPresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun clearSelection() {
 | 
			
		||||
        selection.clear()
 | 
			
		||||
        state.selection = emptyList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun toggleSelection(manga: LibraryManga) {
 | 
			
		||||
        val mutableList = state.selection.toMutableList()
 | 
			
		||||
        if (selection.fastAny { it.id == manga.id }) {
 | 
			
		||||
            selection.remove(manga)
 | 
			
		||||
            mutableList.remove(manga)
 | 
			
		||||
        } else {
 | 
			
		||||
            selection.add(manga)
 | 
			
		||||
            mutableList.add(manga)
 | 
			
		||||
        }
 | 
			
		||||
        view?.invalidateActionMode()
 | 
			
		||||
        view?.createActionModeIfNeeded()
 | 
			
		||||
        state.selection = mutableList
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
        state.selection = state.selection.toMutableList().apply {
 | 
			
		||||
            addAll(items.filterNot { it.manga in selection }.map { it.manga })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
        state.selection = items.filterNot { it in selection }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -488,9 +488,13 @@ class MainActivity : BaseActivity() {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        val backstackSize = router.backstackSize
 | 
			
		||||
        if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
 | 
			
		||||
        val startScreen = router.getControllerWithTag("$startScreenId")
 | 
			
		||||
        if (backstackSize == 1 && startScreen == null) {
 | 
			
		||||
            // Return to start screen
 | 
			
		||||
            moveToStartScreen()
 | 
			
		||||
            setSelectedNavItem(startScreenId)
 | 
			
		||||
        } else if (startScreen != null && router.handleBack()) {
 | 
			
		||||
            // Clear selection for Library screen
 | 
			
		||||
        } else if (shouldHandleExitConfirmation()) {
 | 
			
		||||
            // Exit confirmation (resets after 2 seconds)
 | 
			
		||||
            lifecycleScope.launchUI { resetExitConfirmation() }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user