mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Use Compose on BrowseSourceScreens (#7901)
This commit is contained in:
		@@ -27,6 +27,10 @@ class MangaRepositoryImpl(
 | 
			
		||||
        return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
 | 
			
		||||
        return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getFavorites(): List<Manga> {
 | 
			
		||||
        return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -26,4 +26,8 @@ class GetManga(
 | 
			
		||||
    suspend fun await(url: String, sourceId: Long): Manga? {
 | 
			
		||||
        return mangaRepository.getMangaByUrlAndSourceId(url, sourceId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun subscribe(url: String, sourceId: Long): Flow<Manga?> {
 | 
			
		||||
        return mangaRepository.getMangaByUrlAndSourceIdAsFlow(url, sourceId)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ interface MangaRepository {
 | 
			
		||||
 | 
			
		||||
    suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga?
 | 
			
		||||
 | 
			
		||||
    fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?>
 | 
			
		||||
 | 
			
		||||
    suspend fun getFavorites(): List<Manga>
 | 
			
		||||
 | 
			
		||||
    suspend fun getLibraryManga(): List<LibraryManga>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material3.SnackbarHostState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseLatestToolbar
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseLatestScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val columns by presenter.getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
    val onHelpClick = {
 | 
			
		||||
        uriHandler.openUri(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val onWebViewClick = f@{
 | 
			
		||||
        val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
        val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
        context.startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = {
 | 
			
		||||
            BrowseLatestToolbar(
 | 
			
		||||
                navigateUp = navigateUp,
 | 
			
		||||
                source = presenter.source!!,
 | 
			
		||||
                displayMode = presenter.displayMode,
 | 
			
		||||
                onDisplayModeChange = { presenter.displayMode = it },
 | 
			
		||||
                onHelpClick = onHelpClick,
 | 
			
		||||
                onWebViewClick = onWebViewClick,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        BrowseSourceContent(
 | 
			
		||||
            source = presenter.source,
 | 
			
		||||
            mangaList = presenter.getMangaList().collectAsLazyPagingItems(),
 | 
			
		||||
            getMangaState = { presenter.getManga(it) },
 | 
			
		||||
            columns = columns,
 | 
			
		||||
            displayMode = presenter.displayMode,
 | 
			
		||||
            snackbarHostState = remember { SnackbarHostState() },
 | 
			
		||||
            contentPadding = paddingValues,
 | 
			
		||||
            onWebViewClick = onWebViewClick,
 | 
			
		||||
            onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
 | 
			
		||||
            onLocalSourceHelpClick = onHelpClick,
 | 
			
		||||
            onMangaClick = onMangaClick,
 | 
			
		||||
            onMangaLongClick = onMangaLongClick,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,210 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.navigationBarsPadding
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.outlined.FilterList
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.SnackbarDuration
 | 
			
		||||
import androidx.compose.material3.SnackbarHost
 | 
			
		||||
import androidx.compose.material3.SnackbarHostState
 | 
			
		||||
import androidx.compose.material3.SnackbarResult
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceList
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val columns by presenter.getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
 | 
			
		||||
    val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
 | 
			
		||||
 | 
			
		||||
    val snackbarHostState = remember { SnackbarHostState() }
 | 
			
		||||
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
    val onHelpClick = {
 | 
			
		||||
        uriHandler.openUri(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val onWebViewClick = f@{
 | 
			
		||||
        val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
        val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
        context.startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = {
 | 
			
		||||
            BrowseSourceToolbar(
 | 
			
		||||
                state = presenter,
 | 
			
		||||
                source = presenter.source!!,
 | 
			
		||||
                displayMode = presenter.displayMode,
 | 
			
		||||
                onDisplayModeChange = onDisplayModeChange,
 | 
			
		||||
                navigateUp = navigateUp,
 | 
			
		||||
                onWebViewClick = onWebViewClick,
 | 
			
		||||
                onHelpClick = onHelpClick,
 | 
			
		||||
                onSearch = { presenter.search() },
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        floatingActionButton = {
 | 
			
		||||
            if (presenter.filters.isNotEmpty()) {
 | 
			
		||||
                ExtendedFloatingActionButton(
 | 
			
		||||
                    modifier = Modifier.navigationBarsPadding(),
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_filter)) },
 | 
			
		||||
                    icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
 | 
			
		||||
                    onClick = onFabClick,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        snackbarHost = {
 | 
			
		||||
            SnackbarHost(hostState = snackbarHostState)
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        BrowseSourceContent(
 | 
			
		||||
            source = presenter.source,
 | 
			
		||||
            mangaList = mangaList,
 | 
			
		||||
            getMangaState = { presenter.getManga(it) },
 | 
			
		||||
            columns = columns,
 | 
			
		||||
            displayMode = presenter.displayMode,
 | 
			
		||||
            snackbarHostState = snackbarHostState,
 | 
			
		||||
            contentPadding = paddingValues,
 | 
			
		||||
            onWebViewClick = onWebViewClick,
 | 
			
		||||
            onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
 | 
			
		||||
            onLocalSourceHelpClick = onHelpClick,
 | 
			
		||||
            onMangaClick = onMangaClick,
 | 
			
		||||
            onMangaLongClick = onMangaLongClick,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceContent(
 | 
			
		||||
    source: CatalogueSource?,
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    snackbarHostState: SnackbarHostState,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
    onHelpClick: () -> Unit,
 | 
			
		||||
    onLocalSourceHelpClick: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
 | 
			
		||||
    val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error }
 | 
			
		||||
        ?: mangaList.loadState.append.takeIf { it is LoadState.Error }
 | 
			
		||||
 | 
			
		||||
    val getErrorMessage: (LoadState.Error) -> String = { state ->
 | 
			
		||||
        when {
 | 
			
		||||
            state.error is NoResultsException -> context.getString(R.string.no_results_found)
 | 
			
		||||
            state.error.message == null -> ""
 | 
			
		||||
            state.error.message!!.startsWith("HTTP error") -> "${state.error.message}: ${context.getString(R.string.http_error_hint)}"
 | 
			
		||||
            else -> state.error.message!!
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    LaunchedEffect(errorState) {
 | 
			
		||||
        if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
 | 
			
		||||
            val result = snackbarHostState.showSnackbar(
 | 
			
		||||
                message = getErrorMessage(errorState),
 | 
			
		||||
                actionLabel = context.getString(R.string.action_webview_refresh),
 | 
			
		||||
                duration = SnackbarDuration.Indefinite,
 | 
			
		||||
            )
 | 
			
		||||
            when (result) {
 | 
			
		||||
                SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
 | 
			
		||||
                SnackbarResult.ActionPerformed -> mangaList.refresh()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
 | 
			
		||||
        EmptyScreen(
 | 
			
		||||
            message = getErrorMessage(errorState),
 | 
			
		||||
            actions = if (source is LocalSource) {
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() },
 | 
			
		||||
                )
 | 
			
		||||
            } else {
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp) { mangaList.refresh() },
 | 
			
		||||
                    EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { onWebViewClick() },
 | 
			
		||||
                    EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { onHelpClick() },
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    when (displayMode) {
 | 
			
		||||
        LibraryDisplayMode.ComfortableGrid -> {
 | 
			
		||||
            BrowseSourceComfortableGrid(
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                getMangaState = getMangaState,
 | 
			
		||||
                columns = columns,
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        LibraryDisplayMode.List -> {
 | 
			
		||||
            BrowseSourceList(
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                getMangaState = getMangaState,
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        else -> {
 | 
			
		||||
            BrowseSourceCompactGrid(
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                getMangaState = getMangaState,
 | 
			
		||||
                columns = columns,
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
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.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
interface BrowseSourceState {
 | 
			
		||||
    val source: CatalogueSource?
 | 
			
		||||
    var searchQuery: String?
 | 
			
		||||
    val currentQuery: String
 | 
			
		||||
    val filters: FilterList
 | 
			
		||||
    val filterItems: List<IFlexible<*>>
 | 
			
		||||
    val appliedFilters: FilterList
 | 
			
		||||
    var dialog: BrowseSourcePresenter.Dialog?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
 | 
			
		||||
    return BrowseSourceStateImpl(initialQuery)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class BrowseSourceStateImpl(initialQuery: String?) : BrowseSourceState {
 | 
			
		||||
    override var source: CatalogueSource? by mutableStateOf(null)
 | 
			
		||||
    override var searchQuery: String? by mutableStateOf(initialQuery)
 | 
			
		||||
    override var currentQuery: String by mutableStateOf(initialQuery ?: "")
 | 
			
		||||
    override var filters: FilterList by mutableStateOf(FilterList())
 | 
			
		||||
    override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
 | 
			
		||||
    override var appliedFilters by mutableStateOf(FilterList())
 | 
			
		||||
    override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SourceSearchScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
    onClickManga: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    BrowseSourceScreen(
 | 
			
		||||
        presenter = presenter,
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        onDisplayModeChange = { presenter.displayMode = (it) },
 | 
			
		||||
        onFabClick = onFabClick,
 | 
			
		||||
        onMangaClick = onClickManga,
 | 
			
		||||
        onMangaLongClick = onClickManga,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,105 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.ViewModule
 | 
			
		||||
import androidx.compose.material.icons.outlined.Check
 | 
			
		||||
import androidx.compose.material.icons.outlined.Help
 | 
			
		||||
import androidx.compose.material.icons.outlined.Public
 | 
			
		||||
import androidx.compose.material.icons.outlined.ViewModule
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.AppBarActions
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseLatestToolbar(
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    source: CatalogueSource,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    onHelpClick: () -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    AppBar(
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        title = source.name,
 | 
			
		||||
        actions = {
 | 
			
		||||
            var selectingDisplayMode by remember { mutableStateOf(false) }
 | 
			
		||||
            AppBarActions(
 | 
			
		||||
                actions = listOf(
 | 
			
		||||
                    AppBar.Action(
 | 
			
		||||
                        title = "display_mode",
 | 
			
		||||
                        icon = Icons.Filled.ViewModule,
 | 
			
		||||
                        onClick = { selectingDisplayMode = true },
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (source is LocalSource) {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = "help",
 | 
			
		||||
                            icon = Icons.Outlined.Help,
 | 
			
		||||
                            onClick = onHelpClick,
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = "webview",
 | 
			
		||||
                            icon = Icons.Outlined.Public,
 | 
			
		||||
                            onClick = onWebViewClick,
 | 
			
		||||
                        )
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
            DropdownMenu(
 | 
			
		||||
                expanded = selectingDisplayMode,
 | 
			
		||||
                onDismissRequest = { selectingDisplayMode = false },
 | 
			
		||||
            ) {
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.ComfortableGrid) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_grid)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.CompactGrid) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_list)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.List) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.List) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,106 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.drawWithContent
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.components.Badge
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaGridComfortableText
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaGridCover
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceComfortableGrid(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyVerticalGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
        contentPadding = PaddingValues(8.dp) + contentPadding,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(mangaList.itemCount) { index ->
 | 
			
		||||
            val initialManga = mangaList[index] ?: return@items
 | 
			
		||||
            val manga by getMangaState(initialManga)
 | 
			
		||||
            BrowseSourceComfortableGridItem(
 | 
			
		||||
                manga = manga,
 | 
			
		||||
                onClick = { onMangaClick(manga) },
 | 
			
		||||
                onLongClick = { onMangaLongClick(manga) },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceComfortableGridItem(
 | 
			
		||||
    manga: Manga,
 | 
			
		||||
    onClick: () -> Unit = {},
 | 
			
		||||
    onLongClick: () -> Unit = onClick,
 | 
			
		||||
) {
 | 
			
		||||
    val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .combinedClickable(
 | 
			
		||||
                onClick = onClick,
 | 
			
		||||
                onLongClick = onLongClick,
 | 
			
		||||
            ),
 | 
			
		||||
    ) {
 | 
			
		||||
        MangaGridCover(
 | 
			
		||||
            cover = {
 | 
			
		||||
                MangaCover.Book(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .fillMaxWidth()
 | 
			
		||||
                        .drawWithContent {
 | 
			
		||||
                            drawContent()
 | 
			
		||||
                            if (manga.favorite) {
 | 
			
		||||
                                drawRect(overlayColor)
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                    data = manga.thumbnailUrl,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            badgesStart = {
 | 
			
		||||
                if (manga.favorite) {
 | 
			
		||||
                    Badge(text = stringResource(id = R.string.in_library))
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        MangaGridComfortableText(
 | 
			
		||||
            text = manga.title,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,129 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.draw.drawWithContent
 | 
			
		||||
import androidx.compose.ui.graphics.Brush
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.components.Badge
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaGridCompactText
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaGridCover
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceCompactGrid(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyVerticalGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
        contentPadding = PaddingValues(8.dp) + contentPadding,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(mangaList.itemCount) { index ->
 | 
			
		||||
            val initialManga = mangaList[index] ?: return@items
 | 
			
		||||
            val manga by getMangaState(initialManga)
 | 
			
		||||
            BrowseSourceCompactGridItem(
 | 
			
		||||
                manga = manga,
 | 
			
		||||
                onClick = { onMangaClick(manga) },
 | 
			
		||||
                onLongClick = { onMangaLongClick(manga) },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceCompactGridItem(
 | 
			
		||||
    manga: Manga,
 | 
			
		||||
    onClick: () -> Unit = {},
 | 
			
		||||
    onLongClick: () -> Unit = onClick,
 | 
			
		||||
) {
 | 
			
		||||
    val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
 | 
			
		||||
    MangaGridCover(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .combinedClickable(
 | 
			
		||||
                onClick = onClick,
 | 
			
		||||
                onLongClick = onLongClick,
 | 
			
		||||
            ),
 | 
			
		||||
        cover = {
 | 
			
		||||
            MangaCover.Book(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxHeight()
 | 
			
		||||
                    .drawWithContent {
 | 
			
		||||
                        drawContent()
 | 
			
		||||
                        if (manga.favorite) {
 | 
			
		||||
                            drawRect(overlayColor)
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                data = eu.kanade.domain.manga.model.MangaCover(
 | 
			
		||||
                    manga.id,
 | 
			
		||||
                    manga.source,
 | 
			
		||||
                    manga.favorite,
 | 
			
		||||
                    manga.thumbnailUrl,
 | 
			
		||||
                    manga.coverLastModified,
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        badgesStart = {
 | 
			
		||||
            if (manga.favorite) {
 | 
			
		||||
                Badge(text = stringResource(id = R.string.in_library))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        content = {
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
 | 
			
		||||
                    .background(
 | 
			
		||||
                        Brush.verticalGradient(
 | 
			
		||||
                            0f to Color.Transparent,
 | 
			
		||||
                            1f to Color(0xAA000000),
 | 
			
		||||
                        ),
 | 
			
		||||
                    )
 | 
			
		||||
                    .fillMaxHeight(0.33f)
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .align(Alignment.BottomCenter),
 | 
			
		||||
            )
 | 
			
		||||
            MangaGridCompactText(manga.title)
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material.TextButton
 | 
			
		||||
import androidx.compose.material3.AlertDialog
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun RemoveMangaDialog(
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onConfirm: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    AlertDialog(
 | 
			
		||||
        onDismissRequest = onDismissRequest,
 | 
			
		||||
        dismissButton = {
 | 
			
		||||
            TextButton(onClick = onDismissRequest) {
 | 
			
		||||
                Text(text = stringResource(id = android.R.string.cancel))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        confirmButton = {
 | 
			
		||||
            TextButton(
 | 
			
		||||
                onClick = {
 | 
			
		||||
                    onDismissRequest()
 | 
			
		||||
                    onConfirm()
 | 
			
		||||
                },
 | 
			
		||||
            ) {
 | 
			
		||||
                Text(text = stringResource(id = R.string.action_remove))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        title = {
 | 
			
		||||
            Text(text = stringResource(id = R.string.are_you_sure))
 | 
			
		||||
        },
 | 
			
		||||
        text = {
 | 
			
		||||
            Text(text = stringResource(R.string.remove_manga))
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,94 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.drawWithContent
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import androidx.paging.compose.items
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.components.Badge
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaListItem
 | 
			
		||||
import eu.kanade.presentation.library.components.MangaListItemContent
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import eu.kanade.presentation.util.verticalPadding
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceList(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
    ) {
 | 
			
		||||
        item {
 | 
			
		||||
            if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(mangaList) { initialManga ->
 | 
			
		||||
            initialManga ?: return@items
 | 
			
		||||
            val manga by getMangaState(initialManga)
 | 
			
		||||
            BrowseSourceListItem(
 | 
			
		||||
                manga = manga,
 | 
			
		||||
                onClick = { onMangaClick(manga) },
 | 
			
		||||
                onLongClick = { onMangaLongClick(manga) },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        item {
 | 
			
		||||
            if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceListItem(
 | 
			
		||||
    manga: Manga,
 | 
			
		||||
    onClick: () -> Unit = {},
 | 
			
		||||
    onLongClick: () -> Unit = onClick,
 | 
			
		||||
) {
 | 
			
		||||
    val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
 | 
			
		||||
    MangaListItem(
 | 
			
		||||
        coverContent = {
 | 
			
		||||
            MangaCover.Square(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(vertical = verticalPadding)
 | 
			
		||||
                    .fillMaxHeight()
 | 
			
		||||
                    .drawWithContent {
 | 
			
		||||
                        drawContent()
 | 
			
		||||
                        if (manga.favorite) {
 | 
			
		||||
                            drawRect(overlayColor)
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                data = manga.thumbnailUrl,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        onClick = onClick,
 | 
			
		||||
        onLongClick = onLongClick,
 | 
			
		||||
        badges = {
 | 
			
		||||
            if (manga.favorite) {
 | 
			
		||||
                Badge(text = stringResource(id = R.string.in_library))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        content = {
 | 
			
		||||
            MangaListItemContent(text = manga.title)
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.material3.CircularProgressIndicator
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceLoadingItem() {
 | 
			
		||||
    Row(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .padding(vertical = 16.dp),
 | 
			
		||||
        horizontalArrangement = Arrangement.Center,
 | 
			
		||||
    ) {
 | 
			
		||||
        CircularProgressIndicator(
 | 
			
		||||
            modifier = Modifier.size(64.dp),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,206 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.text.BasicTextField
 | 
			
		||||
import androidx.compose.foundation.text.KeyboardActions
 | 
			
		||||
import androidx.compose.foundation.text.KeyboardOptions
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.ViewModule
 | 
			
		||||
import androidx.compose.material.icons.outlined.Check
 | 
			
		||||
import androidx.compose.material.icons.outlined.Clear
 | 
			
		||||
import androidx.compose.material.icons.outlined.Help
 | 
			
		||||
import androidx.compose.material.icons.outlined.Public
 | 
			
		||||
import androidx.compose.material.icons.outlined.Search
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.focus.FocusRequester
 | 
			
		||||
import androidx.compose.ui.focus.focusRequester
 | 
			
		||||
import androidx.compose.ui.graphics.SolidColor
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.input.ImeAction
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceState
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.AppBarActions
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
import kotlinx.coroutines.delay
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceToolbar(
 | 
			
		||||
    state: BrowseSourceState,
 | 
			
		||||
    source: CatalogueSource,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
    onHelpClick: () -> Unit,
 | 
			
		||||
    onSearch: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    if (state.searchQuery == null) {
 | 
			
		||||
        BrowseSourceRegularToolbar(
 | 
			
		||||
            source = source,
 | 
			
		||||
            displayMode = displayMode,
 | 
			
		||||
            onDisplayModeChange = onDisplayModeChange,
 | 
			
		||||
            navigateUp = navigateUp,
 | 
			
		||||
            onSearchClick = { state.searchQuery = "" },
 | 
			
		||||
            onWebViewClick = onWebViewClick,
 | 
			
		||||
            onHelpClick = onHelpClick,
 | 
			
		||||
        )
 | 
			
		||||
    } else {
 | 
			
		||||
        BrowseSourceSearchToolbar(
 | 
			
		||||
            searchQuery = state.searchQuery!!,
 | 
			
		||||
            onSearchQueryChanged = { state.searchQuery = it },
 | 
			
		||||
            navigateUp = {
 | 
			
		||||
                state.searchQuery = null
 | 
			
		||||
                onSearch()
 | 
			
		||||
            },
 | 
			
		||||
            onResetClick = { state.searchQuery = "" },
 | 
			
		||||
            onSearchClick = onSearch,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceRegularToolbar(
 | 
			
		||||
    source: CatalogueSource,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onSearchClick: () -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
    onHelpClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    AppBar(
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        title = source.name,
 | 
			
		||||
        actions = {
 | 
			
		||||
            var selectingDisplayMode by remember { mutableStateOf(false) }
 | 
			
		||||
            AppBarActions(
 | 
			
		||||
                actions = listOf(
 | 
			
		||||
                    AppBar.Action(
 | 
			
		||||
                        title = "search",
 | 
			
		||||
                        icon = Icons.Outlined.Search,
 | 
			
		||||
                        onClick = onSearchClick,
 | 
			
		||||
                    ),
 | 
			
		||||
                    AppBar.Action(
 | 
			
		||||
                        title = "display_mode",
 | 
			
		||||
                        icon = Icons.Filled.ViewModule,
 | 
			
		||||
                        onClick = { selectingDisplayMode = true },
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (source is LocalSource) {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = "help",
 | 
			
		||||
                            icon = Icons.Outlined.Help,
 | 
			
		||||
                            onClick = onHelpClick,
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = "webview",
 | 
			
		||||
                            icon = Icons.Outlined.Public,
 | 
			
		||||
                            onClick = onWebViewClick,
 | 
			
		||||
                        )
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
            DropdownMenu(
 | 
			
		||||
                expanded = selectingDisplayMode,
 | 
			
		||||
                onDismissRequest = { selectingDisplayMode = false },
 | 
			
		||||
            ) {
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.ComfortableGrid) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_grid)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.CompactGrid) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_list)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.List) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.List) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceSearchToolbar(
 | 
			
		||||
    searchQuery: String,
 | 
			
		||||
    onSearchQueryChanged: (String) -> Unit,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onResetClick: () -> Unit,
 | 
			
		||||
    onSearchClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val focusRequester = remember { FocusRequester() }
 | 
			
		||||
    AppBar(
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        titleContent = {
 | 
			
		||||
            BasicTextField(
 | 
			
		||||
                value = searchQuery,
 | 
			
		||||
                onValueChange = onSearchQueryChanged,
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .focusRequester(focusRequester),
 | 
			
		||||
                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
 | 
			
		||||
                keyboardActions = KeyboardActions(
 | 
			
		||||
                    onSearch = {
 | 
			
		||||
                        onSearchClick()
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        actions = {
 | 
			
		||||
            AppBarActions(
 | 
			
		||||
                actions = listOf(
 | 
			
		||||
                    AppBar.Action(
 | 
			
		||||
                        title = "clear",
 | 
			
		||||
                        icon = Icons.Outlined.Clear,
 | 
			
		||||
                        onClick = onResetClick,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    LaunchedEffect(Unit) {
 | 
			
		||||
        // TODO: https://issuetracker.google.com/issues/204502668
 | 
			
		||||
        delay(100)
 | 
			
		||||
        focusRequester.requestFocus()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.presentation.manga.components
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
@@ -78,13 +78,22 @@ fun LibraryComfortableGridItem(
 | 
			
		||||
                isLocal = item.isLocal,
 | 
			
		||||
                language = item.sourceLanguage,
 | 
			
		||||
            )
 | 
			
		||||
            Text(
 | 
			
		||||
                modifier = Modifier.padding(4.dp),
 | 
			
		||||
            MangaGridComfortableText(
 | 
			
		||||
                text = manga.title,
 | 
			
		||||
                fontSize = 12.sp,
 | 
			
		||||
                maxLines = 2,
 | 
			
		||||
                style = MaterialTheme.typography.titleSmall,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MangaGridComfortableText(
 | 
			
		||||
    text: String,
 | 
			
		||||
) {
 | 
			
		||||
    Text(
 | 
			
		||||
        modifier = Modifier.padding(4.dp),
 | 
			
		||||
        text = text,
 | 
			
		||||
        fontSize = 12.sp,
 | 
			
		||||
        maxLines = 2,
 | 
			
		||||
        style = MaterialTheme.typography.titleSmall,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package eu.kanade.presentation.library.components
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.BoxScope
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
@@ -97,20 +98,27 @@ fun LibraryCompactGridItem(
 | 
			
		||||
                .fillMaxWidth()
 | 
			
		||||
                .align(Alignment.BottomCenter),
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            text = manga.title,
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .padding(8.dp)
 | 
			
		||||
                .align(Alignment.BottomStart),
 | 
			
		||||
            color = Color.White,
 | 
			
		||||
            fontSize = 12.sp,
 | 
			
		||||
            maxLines = 2,
 | 
			
		||||
            style = MaterialTheme.typography.titleSmall.copy(
 | 
			
		||||
                shadow = Shadow(
 | 
			
		||||
                    color = Color.Black,
 | 
			
		||||
                    blurRadius = 4f,
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        MangaGridCompactText(manga.title)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BoxScope.MangaGridCompactText(
 | 
			
		||||
    text: String,
 | 
			
		||||
) {
 | 
			
		||||
    Text(
 | 
			
		||||
        text = text,
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .padding(8.dp)
 | 
			
		||||
            .align(Alignment.BottomStart),
 | 
			
		||||
        color = Color.White,
 | 
			
		||||
        fontSize = 12.sp,
 | 
			
		||||
        maxLines = 2,
 | 
			
		||||
        style = MaterialTheme.typography.titleSmall.copy(
 | 
			
		||||
            shadow = Shadow(
 | 
			
		||||
                color = Color.Black,
 | 
			
		||||
                blurRadius = 4f,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.BoxScope
 | 
			
		||||
import androidx.compose.foundation.layout.RowScope
 | 
			
		||||
import androidx.compose.foundation.layout.aspectRatio
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
@@ -16,6 +17,41 @@ import eu.kanade.presentation.components.BadgeGroup
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MangaGridCover(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    cover: @Composable BoxScope.() -> Unit = {},
 | 
			
		||||
    badgesStart: (@Composable RowScope.() -> Unit)? = null,
 | 
			
		||||
    badgesEnd: (@Composable RowScope.() -> Unit)? = null,
 | 
			
		||||
    content: @Composable BoxScope.() -> Unit = {},
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .aspectRatio(MangaCover.Book.ratio),
 | 
			
		||||
    ) {
 | 
			
		||||
        cover()
 | 
			
		||||
        content()
 | 
			
		||||
        if (badgesStart != null) {
 | 
			
		||||
            BadgeGroup(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(4.dp)
 | 
			
		||||
                    .align(Alignment.TopStart),
 | 
			
		||||
                content = badgesStart,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (badgesEnd != null) {
 | 
			
		||||
            BadgeGroup(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(4.dp)
 | 
			
		||||
                    .align(Alignment.TopEnd),
 | 
			
		||||
                content = badgesEnd,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LibraryGridCover(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
@@ -26,54 +62,41 @@ fun LibraryGridCover(
 | 
			
		||||
    language: String,
 | 
			
		||||
    content: @Composable BoxScope.() -> Unit = {},
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .aspectRatio(MangaCover.Book.ratio),
 | 
			
		||||
    ) {
 | 
			
		||||
        MangaCover.Book(
 | 
			
		||||
            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
            data = mangaCover,
 | 
			
		||||
        )
 | 
			
		||||
        content()
 | 
			
		||||
        if (downloadCount > 0 || unreadCount > 0) {
 | 
			
		||||
            BadgeGroup(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(4.dp)
 | 
			
		||||
                    .align(Alignment.TopStart),
 | 
			
		||||
            ) {
 | 
			
		||||
                if (downloadCount > 0) {
 | 
			
		||||
                    Badge(
 | 
			
		||||
                        text = "$downloadCount",
 | 
			
		||||
                        color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                        textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                if (unreadCount > 0) {
 | 
			
		||||
                    Badge(text = "$unreadCount")
 | 
			
		||||
                }
 | 
			
		||||
    MangaGridCover(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        cover = {
 | 
			
		||||
            MangaCover.Book(
 | 
			
		||||
                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                data = mangaCover,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        badgesStart = {
 | 
			
		||||
            if (downloadCount > 0) {
 | 
			
		||||
                Badge(
 | 
			
		||||
                    text = "$downloadCount",
 | 
			
		||||
                    color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                    textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (isLocal || language.isNotEmpty()) {
 | 
			
		||||
            BadgeGroup(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(4.dp)
 | 
			
		||||
                    .align(Alignment.TopEnd),
 | 
			
		||||
            ) {
 | 
			
		||||
                if (isLocal) {
 | 
			
		||||
                    Badge(
 | 
			
		||||
                        text = stringResource(R.string.local_source_badge),
 | 
			
		||||
                        color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                        textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                    )
 | 
			
		||||
                } else if (language.isNotEmpty()) {
 | 
			
		||||
                    Badge(
 | 
			
		||||
                        text = language,
 | 
			
		||||
                        color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                        textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            if (unreadCount > 0) {
 | 
			
		||||
                Badge(text = "$unreadCount")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        },
 | 
			
		||||
        badgesEnd = {
 | 
			
		||||
            if (isLocal) {
 | 
			
		||||
                Badge(
 | 
			
		||||
                    text = stringResource(R.string.local_source_badge),
 | 
			
		||||
                    color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                    textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                )
 | 
			
		||||
            } else if (language.isNotEmpty()) {
 | 
			
		||||
                Badge(
 | 
			
		||||
                    text = language,
 | 
			
		||||
                    color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                    textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        content = content,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.presentation.library.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.combinedClickable
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.RowScope
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
@@ -19,6 +20,7 @@ import eu.kanade.domain.manga.model.MangaCover
 | 
			
		||||
import eu.kanade.presentation.components.Badge
 | 
			
		||||
import eu.kanade.presentation.components.BadgeGroup
 | 
			
		||||
import eu.kanade.presentation.components.FastScrollLazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaCover.Square
 | 
			
		||||
import eu.kanade.presentation.components.TextButton
 | 
			
		||||
import eu.kanade.presentation.util.bottomNavPaddingValues
 | 
			
		||||
import eu.kanade.presentation.util.horizontalPadding
 | 
			
		||||
@@ -74,62 +76,109 @@ fun LibraryListItem(
 | 
			
		||||
    onLongClick: (LibraryManga) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val manga = item.manga
 | 
			
		||||
    MangaListItem(
 | 
			
		||||
        modifier = Modifier.selectedBackground(isSelected),
 | 
			
		||||
        title = manga.title,
 | 
			
		||||
        cover = MangaCover(
 | 
			
		||||
            manga.id!!,
 | 
			
		||||
            manga.source,
 | 
			
		||||
            manga.favorite,
 | 
			
		||||
            manga.thumbnail_url,
 | 
			
		||||
            manga.cover_last_modified,
 | 
			
		||||
        ),
 | 
			
		||||
        onClick = { onClick(manga) },
 | 
			
		||||
        onLongClick = { onLongClick(manga) },
 | 
			
		||||
    ) {
 | 
			
		||||
        if (item.downloadCount > 0) {
 | 
			
		||||
            Badge(
 | 
			
		||||
                text = "${item.downloadCount}",
 | 
			
		||||
                color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        if (item.unreadCount > 0) {
 | 
			
		||||
            Badge(text = "${item.unreadCount}")
 | 
			
		||||
        }
 | 
			
		||||
        if (item.isLocal) {
 | 
			
		||||
            Badge(
 | 
			
		||||
                text = stringResource(R.string.local_source_badge),
 | 
			
		||||
                color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) {
 | 
			
		||||
            Badge(
 | 
			
		||||
                text = item.sourceLanguage,
 | 
			
		||||
                color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MangaListItem(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    title: String,
 | 
			
		||||
    cover: MangaCover,
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
    onLongClick: () -> Unit = onClick,
 | 
			
		||||
    badges: @Composable RowScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    MangaListItem(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        coverContent = {
 | 
			
		||||
            Square(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(vertical = verticalPadding)
 | 
			
		||||
                    .fillMaxHeight(),
 | 
			
		||||
                data = cover,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        badges = badges,
 | 
			
		||||
        onClick = onClick,
 | 
			
		||||
        onLongClick = onLongClick,
 | 
			
		||||
        content = {
 | 
			
		||||
            MangaListItemContent(title)
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MangaListItem(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    coverContent: @Composable RowScope.() -> Unit,
 | 
			
		||||
    badges: @Composable RowScope.() -> Unit,
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
    onLongClick: () -> Unit,
 | 
			
		||||
    content: @Composable RowScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Row(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .selectedBackground(isSelected)
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .height(56.dp)
 | 
			
		||||
            .combinedClickable(
 | 
			
		||||
                onClick = { onClick(manga) },
 | 
			
		||||
                onLongClick = { onLongClick(manga) },
 | 
			
		||||
                onClick = onClick,
 | 
			
		||||
                onLongClick = onLongClick,
 | 
			
		||||
            )
 | 
			
		||||
            .padding(horizontal = horizontalPadding),
 | 
			
		||||
        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
    ) {
 | 
			
		||||
        eu.kanade.presentation.components.MangaCover.Square(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .padding(vertical = verticalPadding)
 | 
			
		||||
                .fillMaxHeight(),
 | 
			
		||||
            data = MangaCover(
 | 
			
		||||
                manga.id!!,
 | 
			
		||||
                manga.source,
 | 
			
		||||
                manga.favorite,
 | 
			
		||||
                manga.thumbnail_url,
 | 
			
		||||
                manga.cover_last_modified,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            text = manga.title,
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .padding(horizontal = horizontalPadding)
 | 
			
		||||
                .weight(1f),
 | 
			
		||||
            maxLines = 2,
 | 
			
		||||
            style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
        )
 | 
			
		||||
        BadgeGroup {
 | 
			
		||||
            if (item.downloadCount > 0) {
 | 
			
		||||
                Badge(
 | 
			
		||||
                    text = "${item.downloadCount}",
 | 
			
		||||
                    color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                    textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (item.unreadCount > 0) {
 | 
			
		||||
                Badge(text = "${item.unreadCount}")
 | 
			
		||||
            }
 | 
			
		||||
            if (item.isLocal) {
 | 
			
		||||
                Badge(
 | 
			
		||||
                    text = stringResource(R.string.local_source_badge),
 | 
			
		||||
                    color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                    textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) {
 | 
			
		||||
                Badge(
 | 
			
		||||
                    text = item.sourceLanguage,
 | 
			
		||||
                    color = MaterialTheme.colorScheme.tertiary,
 | 
			
		||||
                    textColor = MaterialTheme.colorScheme.onTertiary,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        coverContent()
 | 
			
		||||
        content()
 | 
			
		||||
        BadgeGroup(content = badges)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun RowScope.MangaListItemContent(
 | 
			
		||||
    text: String,
 | 
			
		||||
) {
 | 
			
		||||
    Text(
 | 
			
		||||
        text = text,
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .padding(horizontal = horizontalPadding)
 | 
			
		||||
            .weight(1f),
 | 
			
		||||
        maxLines = 2,
 | 
			
		||||
        style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga as DomainManga
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to create cover cache.
 | 
			
		||||
@@ -87,6 +88,20 @@ class CoverCache(private val context: Context) {
 | 
			
		||||
        return deleted
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteFromCache(manga: DomainManga, deleteCustomCover: Boolean = false): Int {
 | 
			
		||||
        var amountDeleted = 0
 | 
			
		||||
 | 
			
		||||
        getCoverFile(manga.thumbnailUrl)?.let {
 | 
			
		||||
            if (it.exists() && it.delete()) amountDeleted++
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (deleteCustomCover && deleteCustomCover(manga.id)) {
 | 
			
		||||
            amountDeleted++
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return amountDeleted
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete custom cover of the manga from the cache
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.migration.search
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.core.os.bundleOf
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.SourceSearchScreen
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getSerializableCompat
 | 
			
		||||
 | 
			
		||||
class SourceSearchController(
 | 
			
		||||
@@ -13,30 +15,34 @@ class SourceSearchController(
 | 
			
		||||
) : BrowseSourceController(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
 | 
			
		||||
        Bundle().apply {
 | 
			
		||||
            putLong(SOURCE_ID_KEY, source.id)
 | 
			
		||||
            putSerializable(MANGA_KEY, manga)
 | 
			
		||||
            if (searchQuery != null) {
 | 
			
		||||
                putString(SEARCH_QUERY_KEY, searchQuery)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        bundleOf(
 | 
			
		||||
            SOURCE_ID_KEY to source.id,
 | 
			
		||||
            MANGA_KEY to manga,
 | 
			
		||||
            SEARCH_QUERY_KEY to searchQuery,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private var oldManga: Manga? = args.getSerializableCompat(MANGA_KEY)
 | 
			
		||||
    private var newManga: Manga? = null
 | 
			
		||||
 | 
			
		||||
    override fun onItemClick(view: View, position: Int): Boolean {
 | 
			
		||||
        val item = adapter?.getItem(position) as? SourceItem ?: return false
 | 
			
		||||
        newManga = item.manga
 | 
			
		||||
        val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
 | 
			
		||||
        val dialog =
 | 
			
		||||
            SearchController.MigrationDialog(oldManga, newManga, this)
 | 
			
		||||
        dialog.targetController = searchController
 | 
			
		||||
        dialog.showDialog(router)
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        SourceSearchScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onFabClick = { filterSheet?.show() },
 | 
			
		||||
            onClickManga = {
 | 
			
		||||
                newManga = it
 | 
			
		||||
                val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
 | 
			
		||||
                val dialog = SearchController.MigrationDialog(oldManga, newManga, this)
 | 
			
		||||
                dialog.targetController = searchController
 | 
			
		||||
                dialog.showDialog(router)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    override fun onItemLongClick(position: Int) {
 | 
			
		||||
        view?.let { super.onItemClick(it, position) }
 | 
			
		||||
        LaunchedEffect(presenter.filters) {
 | 
			
		||||
            initFilterSheet()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import androidx.paging.PagingState
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withIOContext
 | 
			
		||||
 | 
			
		||||
abstract class BrowsePagingSource : PagingSource<Long, SManga>() {
 | 
			
		||||
 | 
			
		||||
    abstract suspend fun requestNextPage(currentPage: Int): MangasPage
 | 
			
		||||
 | 
			
		||||
    override suspend fun load(params: LoadParams<Long>): LoadResult<Long, SManga> {
 | 
			
		||||
        val page = params.key ?: 1
 | 
			
		||||
 | 
			
		||||
        val mangasPage = try {
 | 
			
		||||
            withIOContext {
 | 
			
		||||
                requestNextPage(page.toInt())
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            return LoadResult.Error(e)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return LoadResult.Page(
 | 
			
		||||
            data = mangasPage.mangas,
 | 
			
		||||
            prevKey = null,
 | 
			
		||||
            nextKey = if (mangasPage.hasNextPage) page + 1 else null,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getRefreshKey(state: PagingState<Long, SManga>): Long? {
 | 
			
		||||
        return state.anchorPosition?.let { anchorPosition ->
 | 
			
		||||
            val anchorPage = state.closestPageToPosition(anchorPosition)
 | 
			
		||||
            anchorPage?.prevKey ?: anchorPage?.nextKey
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,328 +1,125 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
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 android.view.ViewGroup
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.core.view.updatePadding
 | 
			
		||||
import androidx.recyclerview.widget.GridLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.fredporciuncula.flow.preferences.Preference
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import dev.chrisbanes.insetter.applyInsetter
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.domain.category.model.Category
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.domain.manga.model.toDbManga
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.core.os.bundleOf
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceScreen
 | 
			
		||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.MainActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.AddDuplicateMangaDialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Dialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchIO
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withUIContext
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.asHotFlow
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.connectivityManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.logcat
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.openInBrowser
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.snack
 | 
			
		||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 | 
			
		||||
import eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.flow.drop
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
    SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
 | 
			
		||||
    FabController,
 | 
			
		||||
    FlexibleAdapter.OnItemClickListener,
 | 
			
		||||
    FlexibleAdapter.OnItemLongClickListener,
 | 
			
		||||
    FlexibleAdapter.EndlessScrollListener,
 | 
			
		||||
    ChangeMangaCategoriesDialog.Listener {
 | 
			
		||||
    FullComposeController<BrowseSourcePresenter>(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(sourceId: Long, query: String? = null) : this(
 | 
			
		||||
        Bundle().apply {
 | 
			
		||||
            putLong(SOURCE_ID_KEY, sourceId)
 | 
			
		||||
            query?.let { query ->
 | 
			
		||||
                putString(SEARCH_QUERY_KEY, query)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        bundleOf(
 | 
			
		||||
            SOURCE_ID_KEY to sourceId,
 | 
			
		||||
            SEARCH_QUERY_KEY to query,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    constructor(source: CatalogueSource, query: String? = null) : this(source.id, query)
 | 
			
		||||
 | 
			
		||||
    constructor(source: Source, query: String? = null) : this(source.id, query)
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing the list of manga from the catalogue.
 | 
			
		||||
     */
 | 
			
		||||
    protected var adapter: FlexibleAdapter<IFlexible<*>>? = null
 | 
			
		||||
 | 
			
		||||
    private var actionFab: ExtendedFloatingActionButton? = null
 | 
			
		||||
    private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Snackbar containing an error message when a request fails.
 | 
			
		||||
     */
 | 
			
		||||
    private var snack: Snackbar? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sheet containing filter items.
 | 
			
		||||
     */
 | 
			
		||||
    private var filterSheet: SourceFilterSheet? = null
 | 
			
		||||
    protected var filterSheet: SourceFilterSheet? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Recycler view with the list of results.
 | 
			
		||||
     */
 | 
			
		||||
    private var recycler: RecyclerView? = null
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the number of manga per row.
 | 
			
		||||
     */
 | 
			
		||||
    private var numColumnsJob: Job? = null
 | 
			
		||||
        BrowseSourceScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onDisplayModeChange = { presenter.displayMode = (it) },
 | 
			
		||||
            onFabClick = { filterSheet?.show() },
 | 
			
		||||
            onMangaClick = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
            onMangaLongClick = { manga ->
 | 
			
		||||
                scope.launchIO {
 | 
			
		||||
                    val duplicateManga = presenter.getDuplicateLibraryManga(manga)
 | 
			
		||||
                    when {
 | 
			
		||||
                        manga.favorite -> presenter.dialog = Dialog.RemoveManga(manga)
 | 
			
		||||
                        duplicateManga != null -> presenter.dialog = Dialog.AddDuplicateManga(manga, duplicateManga)
 | 
			
		||||
                        else -> presenter.addFavorite(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Endless loading item.
 | 
			
		||||
     */
 | 
			
		||||
    private var progressItem: ProgressItem? = null
 | 
			
		||||
        val onDismissRequest = { presenter.dialog = null }
 | 
			
		||||
        when (val dialog = presenter.dialog) {
 | 
			
		||||
            is Dialog.AddDuplicateManga -> {
 | 
			
		||||
                DuplicateMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = { presenter.addFavorite(dialog.manga) },
 | 
			
		||||
                    onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
 | 
			
		||||
                    duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is Dialog.RemoveManga -> {
 | 
			
		||||
                RemoveMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is Dialog.ChangeMangaCategory -> {
 | 
			
		||||
                ChangeCategoryDialog(
 | 
			
		||||
                    initialSelection = dialog.initialSelection,
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onEditCategories = {
 | 
			
		||||
                        router.pushController(CategoryController())
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = { include, _ ->
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                        presenter.moveMangaToCategories(dialog.manga, include)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            null -> {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return presenter.source.name
 | 
			
		||||
        LaunchedEffect(presenter.filters) {
 | 
			
		||||
            initFilterSheet()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseSourcePresenter {
 | 
			
		||||
        return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater)
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        // Initialize adapter, scroll listener and recycler views
 | 
			
		||||
        adapter = FlexibleAdapter(null, this)
 | 
			
		||||
        setupRecycler(view)
 | 
			
		||||
 | 
			
		||||
        binding.progress.isVisible = true
 | 
			
		||||
 | 
			
		||||
        presenter.restartPager()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun initFilterSheet() {
 | 
			
		||||
        if (presenter.sourceFilters.isEmpty()) {
 | 
			
		||||
        if (presenter.filters.isEmpty()) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        filterSheet = SourceFilterSheet(
 | 
			
		||||
            activity!!,
 | 
			
		||||
            onFilterClicked = {
 | 
			
		||||
                showProgressBar()
 | 
			
		||||
                adapter?.clear()
 | 
			
		||||
                presenter.setSourceFilter(presenter.sourceFilters)
 | 
			
		||||
                presenter.setSourceFilter(presenter.filters)
 | 
			
		||||
            },
 | 
			
		||||
            onResetClicked = {
 | 
			
		||||
                presenter.appliedFilters = FilterList()
 | 
			
		||||
                val newFilters = presenter.source.getFilterList()
 | 
			
		||||
                presenter.sourceFilters = newFilters
 | 
			
		||||
                presenter.resetFilter()
 | 
			
		||||
                filterSheet?.setFilters(presenter.filterItems)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filterSheet?.setFilters(presenter.filterItems)
 | 
			
		||||
 | 
			
		||||
        filterSheet?.setOnShowListener { actionFab?.hide() }
 | 
			
		||||
        filterSheet?.setOnDismissListener { actionFab?.show() }
 | 
			
		||||
 | 
			
		||||
        actionFab?.setOnClickListener { filterSheet?.show() }
 | 
			
		||||
 | 
			
		||||
        actionFab?.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun configureFab(fab: ExtendedFloatingActionButton) {
 | 
			
		||||
        actionFab = fab
 | 
			
		||||
 | 
			
		||||
        fab.setText(R.string.action_filter)
 | 
			
		||||
        fab.setIconResource(R.drawable.ic_filter_list_24dp)
 | 
			
		||||
 | 
			
		||||
        // Controlled by initFilterSheet()
 | 
			
		||||
        fab.hide()
 | 
			
		||||
        initFilterSheet()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupFab(fab: ExtendedFloatingActionButton) {
 | 
			
		||||
        fab.setOnClickListener(null)
 | 
			
		||||
        actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
 | 
			
		||||
        actionFab = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        numColumnsJob?.cancel()
 | 
			
		||||
        numColumnsJob = null
 | 
			
		||||
        adapter = null
 | 
			
		||||
        snack = null
 | 
			
		||||
        recycler = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupRecycler(view: View) {
 | 
			
		||||
        numColumnsJob?.cancel()
 | 
			
		||||
 | 
			
		||||
        var oldPosition = RecyclerView.NO_POSITION
 | 
			
		||||
        val oldRecycler = binding.catalogueView.getChildAt(1)
 | 
			
		||||
        if (oldRecycler is RecyclerView) {
 | 
			
		||||
            oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
 | 
			
		||||
            oldRecycler.adapter = null
 | 
			
		||||
 | 
			
		||||
            binding.catalogueView.removeView(oldRecycler)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val recycler = if (preferences.sourceDisplayMode().get() == LibraryDisplayMode.List) {
 | 
			
		||||
            RecyclerView(view.context).apply {
 | 
			
		||||
                id = R.id.recycler
 | 
			
		||||
                layoutManager = LinearLayoutManager(context)
 | 
			
		||||
                layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            (binding.catalogueView.inflate(R.layout.source_recycler_autofit) as AutofitRecyclerView).apply {
 | 
			
		||||
                numColumnsJob = getColumnsPreferenceForCurrentOrientation().asHotFlow { spanCount = it }
 | 
			
		||||
                    .drop(1)
 | 
			
		||||
                    // Set again the adapter to recalculate the covers height
 | 
			
		||||
                    .onEach { adapter = this@BrowseSourceController.adapter }
 | 
			
		||||
                    .launchIn(viewScope)
 | 
			
		||||
 | 
			
		||||
                (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
 | 
			
		||||
                    override fun getSpanSize(position: Int): Int {
 | 
			
		||||
                        return when (adapter?.getItemViewType(position)) {
 | 
			
		||||
                            R.layout.source_compact_grid_item, R.layout.source_comfortable_grid_item -> 1
 | 
			
		||||
                            else -> spanCount
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (filterSheet != null) {
 | 
			
		||||
            // Add bottom padding if filter FAB is visible
 | 
			
		||||
            recycler.updatePadding(bottom = view.resources.getDimensionPixelOffset(R.dimen.fab_list_padding))
 | 
			
		||||
            recycler.clipToPadding = false
 | 
			
		||||
 | 
			
		||||
            actionFab?.shrinkOnScroll(recycler)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        recycler.applyInsetter {
 | 
			
		||||
            type(navigationBars = true) {
 | 
			
		||||
                padding()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
 | 
			
		||||
        binding.catalogueView.addView(recycler, 1)
 | 
			
		||||
 | 
			
		||||
        if (oldPosition != RecyclerView.NO_POSITION) {
 | 
			
		||||
            recycler.layoutManager?.scrollToPosition(oldPosition)
 | 
			
		||||
        }
 | 
			
		||||
        this.recycler = recycler
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
        createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
 | 
			
		||||
        val searchItem = menu.findItem(R.id.action_search)
 | 
			
		||||
 | 
			
		||||
        searchItem.fixExpand(
 | 
			
		||||
            onExpand = { invalidateMenuOnExpand() },
 | 
			
		||||
            onCollapse = {
 | 
			
		||||
                if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller is GlobalSearchController) {
 | 
			
		||||
                    router.popController(this)
 | 
			
		||||
                } else {
 | 
			
		||||
                    nonSubmittedQuery = ""
 | 
			
		||||
                    searchWithQuery("")
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                true
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val displayItem = when (preferences.sourceDisplayMode().get()) {
 | 
			
		||||
            LibraryDisplayMode.List -> R.id.action_list
 | 
			
		||||
            LibraryDisplayMode.ComfortableGrid -> R.id.action_comfortable_grid
 | 
			
		||||
            else -> R.id.action_compact_grid
 | 
			
		||||
        }
 | 
			
		||||
        menu.findItem(displayItem).isChecked = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSearchViewQueryTextSubmit(query: String?) {
 | 
			
		||||
        searchWithQuery(query ?: "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        super.onPrepareOptionsMenu(menu)
 | 
			
		||||
 | 
			
		||||
        val isHttpSource = presenter.source is HttpSource
 | 
			
		||||
        menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
 | 
			
		||||
 | 
			
		||||
        val isLocalSource = presenter.source is LocalSource
 | 
			
		||||
        menu.findItem(R.id.action_local_source_help).isVisible = isLocalSource
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_search -> expandActionViewFromInteraction = true
 | 
			
		||||
            R.id.action_compact_grid -> setDisplayMode(LibraryDisplayMode.CompactGrid)
 | 
			
		||||
            R.id.action_comfortable_grid -> setDisplayMode(LibraryDisplayMode.ComfortableGrid)
 | 
			
		||||
            R.id.action_list -> setDisplayMode(LibraryDisplayMode.List)
 | 
			
		||||
            R.id.action_open_in_web_view -> openInWebView()
 | 
			
		||||
            R.id.action_local_source_help -> openLocalSourceHelpGuide()
 | 
			
		||||
        }
 | 
			
		||||
        return super.onOptionsItemSelected(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openInWebView() {
 | 
			
		||||
        val source = presenter.source as? HttpSource ?: return
 | 
			
		||||
 | 
			
		||||
        val activity = activity ?: return
 | 
			
		||||
        val intent = WebViewActivity.newIntent(activity, source.baseUrl, source.id, presenter.source.name)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openLocalSourceHelpGuide() {
 | 
			
		||||
        activity?.openInBrowser(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -331,15 +128,8 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     * @param newQuery the new query.
 | 
			
		||||
     */
 | 
			
		||||
    fun searchWithQuery(newQuery: String) {
 | 
			
		||||
        // If text didn't change, do nothing
 | 
			
		||||
        if (presenter.query == newQuery) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        showProgressBar()
 | 
			
		||||
        adapter?.clear()
 | 
			
		||||
 | 
			
		||||
        presenter.restartPager(newQuery, presenter.sourceFilters)
 | 
			
		||||
        presenter.searchQuery = newQuery
 | 
			
		||||
        presenter.search()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -350,7 +140,7 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     * @param genreName the name of the genre
 | 
			
		||||
     */
 | 
			
		||||
    fun searchWithGenre(genreName: String) {
 | 
			
		||||
        val defaultFilters = presenter.source.getFilterList()
 | 
			
		||||
        val defaultFilters = presenter.source!!.getFilterList()
 | 
			
		||||
 | 
			
		||||
        var genreExists = false
 | 
			
		||||
 | 
			
		||||
@@ -380,320 +170,15 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (genreExists) {
 | 
			
		||||
            presenter.sourceFilters = defaultFilters
 | 
			
		||||
            filterSheet?.setFilters(presenter.filterItems)
 | 
			
		||||
 | 
			
		||||
            showProgressBar()
 | 
			
		||||
 | 
			
		||||
            adapter?.clear()
 | 
			
		||||
            presenter.restartPager("", defaultFilters)
 | 
			
		||||
            presenter.searchQuery = ""
 | 
			
		||||
            presenter.setFilter(defaultFilters)
 | 
			
		||||
        } else {
 | 
			
		||||
            searchWithQuery(genreName)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called from the presenter when the network request is received.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the current page.
 | 
			
		||||
     * @param mangas the list of manga of the page.
 | 
			
		||||
     */
 | 
			
		||||
    fun onAddPage(page: Int, mangas: List<SourceItem>) {
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        hideProgressBar()
 | 
			
		||||
        if (page == 1) {
 | 
			
		||||
            adapter.clear()
 | 
			
		||||
            resetProgressItem()
 | 
			
		||||
        }
 | 
			
		||||
        adapter.onLoadMoreComplete(mangas)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called from the presenter when the network request fails.
 | 
			
		||||
     *
 | 
			
		||||
     * @param error the error received.
 | 
			
		||||
     */
 | 
			
		||||
    fun onAddPageError(error: Throwable) {
 | 
			
		||||
        logcat(LogPriority.ERROR, error)
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        adapter.onLoadMoreComplete(null)
 | 
			
		||||
        hideProgressBar()
 | 
			
		||||
 | 
			
		||||
        snack?.dismiss()
 | 
			
		||||
 | 
			
		||||
        val message = getErrorMessage(error)
 | 
			
		||||
        val retryAction = View.OnClickListener {
 | 
			
		||||
            // If not the first page, show bottom progress bar.
 | 
			
		||||
            if (adapter.mainItemCount > 0 && progressItem != null) {
 | 
			
		||||
                adapter.addScrollableFooterWithDelay(progressItem!!, 0, true)
 | 
			
		||||
            } else {
 | 
			
		||||
                showProgressBar()
 | 
			
		||||
            }
 | 
			
		||||
            presenter.requestNext()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (adapter.isEmpty) {
 | 
			
		||||
            val actions = if (presenter.source is LocalSource) {
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { openLocalSourceHelpGuide() },
 | 
			
		||||
                )
 | 
			
		||||
            } else {
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp, retryAction),
 | 
			
		||||
                    EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { openInWebView() },
 | 
			
		||||
                    EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { activity?.openInBrowser(MoreController.URL_HELP) },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            binding.emptyView.show(message, actions)
 | 
			
		||||
        } else {
 | 
			
		||||
            snack = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(message, Snackbar.LENGTH_INDEFINITE) {
 | 
			
		||||
                setAction(R.string.action_retry, retryAction)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getErrorMessage(error: Throwable): String {
 | 
			
		||||
        if (error is NoResultsException) {
 | 
			
		||||
            return binding.catalogueView.context.getString(R.string.no_results_found)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return when {
 | 
			
		||||
            error.message == null -> ""
 | 
			
		||||
            error.message!!.startsWith("HTTP error") -> "${error.message}: ${binding.catalogueView.context.getString(R.string.http_error_hint)}"
 | 
			
		||||
            else -> error.message!!
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets a new progress item and reenables the scroll listener.
 | 
			
		||||
     */
 | 
			
		||||
    private fun resetProgressItem() {
 | 
			
		||||
        progressItem = ProgressItem()
 | 
			
		||||
        adapter?.endlessTargetCount = 0
 | 
			
		||||
        adapter?.setEndlessScrollListener(this, progressItem!!)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called by the adapter when scrolled near the bottom.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onLoadMore(lastPosition: Int, currentPage: Int) {
 | 
			
		||||
        if (presenter.hasNextPage()) {
 | 
			
		||||
            presenter.requestNext()
 | 
			
		||||
        } else {
 | 
			
		||||
            adapter?.onLoadMoreComplete(null)
 | 
			
		||||
            adapter?.endlessTargetCount = 1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun noMoreLoad(newItemsSize: Int) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called from the presenter when a manga is initialized.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga initialized
 | 
			
		||||
     */
 | 
			
		||||
    fun onMangaInitialized(manga: Manga) {
 | 
			
		||||
        getHolder(manga)?.setImage(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the current display mode.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mode the mode to change to
 | 
			
		||||
     */
 | 
			
		||||
    private fun setDisplayMode(mode: LibraryDisplayMode) {
 | 
			
		||||
        val view = view ?: return
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
 | 
			
		||||
        preferences.sourceDisplayMode().set(mode)
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
        setupRecycler(view)
 | 
			
		||||
 | 
			
		||||
        // Initialize mangas if not on a metered connection
 | 
			
		||||
        if (!view.context.connectivityManager.isActiveNetworkMetered) {
 | 
			
		||||
            val mangas = (0 until adapter.itemCount).mapNotNull {
 | 
			
		||||
                (adapter.getItem(it) as? SourceItem)?.manga
 | 
			
		||||
            }
 | 
			
		||||
            presenter.initializeMangas(mangas)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a preference for the number of manga per row based on the current orientation.
 | 
			
		||||
     *
 | 
			
		||||
     * @return the preference.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
 | 
			
		||||
        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
 | 
			
		||||
            preferences.portraitColumns()
 | 
			
		||||
        } else {
 | 
			
		||||
            preferences.landscapeColumns()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the view holder for the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to find.
 | 
			
		||||
     * @return the holder of the manga or null if it's not bound.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getHolder(manga: Manga): SourceHolder<*>? {
 | 
			
		||||
        val adapter = adapter ?: return null
 | 
			
		||||
 | 
			
		||||
        adapter.allBoundViewHolders.forEach { holder ->
 | 
			
		||||
            val item = adapter.getItem(holder.bindingAdapterPosition) as? SourceItem
 | 
			
		||||
            if (item != null && item.manga.id == manga.id) {
 | 
			
		||||
                return holder as SourceHolder<*>
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the progress bar.
 | 
			
		||||
     */
 | 
			
		||||
    private fun showProgressBar() {
 | 
			
		||||
        binding.emptyView.hide()
 | 
			
		||||
        binding.progress.isVisible = true
 | 
			
		||||
        snack?.dismiss()
 | 
			
		||||
        snack = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Hides active progress bars.
 | 
			
		||||
     */
 | 
			
		||||
    private fun hideProgressBar() {
 | 
			
		||||
        binding.emptyView.hide()
 | 
			
		||||
        binding.progress.isVisible = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a manga is clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element clicked.
 | 
			
		||||
     * @return true if the item should be selected, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onItemClick(view: View, position: Int): Boolean {
 | 
			
		||||
        val item = adapter?.getItem(position) as? SourceItem ?: return false
 | 
			
		||||
        router.pushController(MangaController(item.manga.id, true))
 | 
			
		||||
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a manga is long clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
 | 
			
		||||
     * in, the list consists of the default category plus the user's categories. The default category is preselected on
 | 
			
		||||
     * new manga, and on already favorited manga the manga's categories are preselected.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element clicked.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onItemLongClick(position: Int) {
 | 
			
		||||
        val activity = activity ?: return
 | 
			
		||||
        val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
 | 
			
		||||
        viewScope.launchIO {
 | 
			
		||||
            val duplicateManga = presenter.getDuplicateLibraryManga(manga)
 | 
			
		||||
 | 
			
		||||
            withUIContext {
 | 
			
		||||
                if (manga.favorite) {
 | 
			
		||||
                    MaterialAlertDialogBuilder(activity)
 | 
			
		||||
                        .setTitle(manga.title)
 | 
			
		||||
                        .setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
 | 
			
		||||
                            when (which) {
 | 
			
		||||
                                0 -> {
 | 
			
		||||
                                    presenter.changeMangaFavorite(manga.toDbManga())
 | 
			
		||||
                                    adapter?.notifyItemChanged(position)
 | 
			
		||||
                                    activity.toast(activity.getString(R.string.manga_removed_library))
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        .show()
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (duplicateManga != null) {
 | 
			
		||||
                        AddDuplicateMangaDialog(this@BrowseSourceController, duplicateManga) {
 | 
			
		||||
                            addToLibrary(
 | 
			
		||||
                                manga,
 | 
			
		||||
                                position,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                            .showDialog(router)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        addToLibrary(manga, position)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun addToLibrary(newManga: Manga, position: Int) {
 | 
			
		||||
        val activity = activity ?: return
 | 
			
		||||
        viewScope.launchIO {
 | 
			
		||||
            val categories = presenter.getCategories()
 | 
			
		||||
            val defaultCategoryId = preferences.defaultCategory()
 | 
			
		||||
            val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
 | 
			
		||||
 | 
			
		||||
            withUIContext {
 | 
			
		||||
                when {
 | 
			
		||||
                    // Default category set
 | 
			
		||||
                    defaultCategory != null -> {
 | 
			
		||||
                        presenter.moveMangaToCategory(newManga.toDbManga(), defaultCategory)
 | 
			
		||||
 | 
			
		||||
                        presenter.changeMangaFavorite(newManga.toDbManga())
 | 
			
		||||
                        adapter?.notifyItemChanged(position)
 | 
			
		||||
                        activity.toast(activity.getString(R.string.manga_added_library))
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Automatic 'Default' or no categories
 | 
			
		||||
                    defaultCategoryId == 0 || categories.isEmpty() -> {
 | 
			
		||||
                        presenter.moveMangaToCategory(newManga.toDbManga(), null)
 | 
			
		||||
 | 
			
		||||
                        presenter.changeMangaFavorite(newManga.toDbManga())
 | 
			
		||||
                        adapter?.notifyItemChanged(position)
 | 
			
		||||
                        activity.toast(activity.getString(R.string.manga_added_library))
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Choose a category
 | 
			
		||||
                    else -> {
 | 
			
		||||
                        val ids = presenter.getMangaCategoryIds(newManga)
 | 
			
		||||
                        val preselected = categories.map {
 | 
			
		||||
                            if (it.id in ids) {
 | 
			
		||||
                                QuadStateTextView.State.CHECKED.ordinal
 | 
			
		||||
                            } else {
 | 
			
		||||
                                QuadStateTextView.State.UNCHECKED.ordinal
 | 
			
		||||
                            }
 | 
			
		||||
                        }.toTypedArray()
 | 
			
		||||
 | 
			
		||||
                        ChangeMangaCategoriesDialog(this@BrowseSourceController, listOf(newManga), categories, preselected)
 | 
			
		||||
                            .showDialog(router)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update manga to use selected categories.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mangas The list of manga to move to categories.
 | 
			
		||||
     * @param categories The list of categories where manga will be placed.
 | 
			
		||||
     */
 | 
			
		||||
    override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
 | 
			
		||||
        val manga = mangas.firstOrNull() ?: return
 | 
			
		||||
 | 
			
		||||
        presenter.changeMangaFavorite(manga.toDbManga())
 | 
			
		||||
        presenter.updateMangaCategories(manga.toDbManga(), addCategories)
 | 
			
		||||
 | 
			
		||||
        val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id }
 | 
			
		||||
        if (position != null) {
 | 
			
		||||
            adapter?.notifyItemChanged(position)
 | 
			
		||||
        }
 | 
			
		||||
        activity?.toast(activity?.getString(R.string.manga_added_library))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected companion object {
 | 
			
		||||
        const val SOURCE_ID_KEY = "sourceId"
 | 
			
		||||
        const val SEARCH_QUERY_KEY = "searchQuery"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,25 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.produceState
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.Pager
 | 
			
		||||
import androidx.paging.PagingConfig
 | 
			
		||||
import androidx.paging.PagingData
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import androidx.paging.cachedIn
 | 
			
		||||
import androidx.paging.map
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.core.prefs.CheckboxState
 | 
			
		||||
import eu.kanade.core.prefs.mapAsCheckboxState
 | 
			
		||||
import eu.kanade.domain.category.interactor.GetCategories
 | 
			
		||||
import eu.kanade.domain.category.interactor.SetMangaCategories
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
 | 
			
		||||
@@ -14,6 +32,8 @@ import eu.kanade.domain.manga.model.toDbManga
 | 
			
		||||
import eu.kanade.domain.manga.model.toMangaUpdate
 | 
			
		||||
import eu.kanade.domain.track.interactor.InsertTrack
 | 
			
		||||
import eu.kanade.domain.track.model.toDomainTrack
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceState
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceStateImpl
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.CoverCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
 | 
			
		||||
@@ -22,6 +42,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
@@ -42,19 +63,17 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
 | 
			
		||||
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchIO
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withUIContext
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withIOContext
 | 
			
		||||
import eu.kanade.tachiyomi.util.removeCovers
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.logcat
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.flow.asFlow
 | 
			
		||||
import kotlinx.coroutines.flow.catch
 | 
			
		||||
import kotlinx.coroutines.flow.collect
 | 
			
		||||
import kotlinx.coroutines.NonCancellable
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.filter
 | 
			
		||||
import kotlinx.coroutines.flow.firstOrNull
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import kotlinx.coroutines.flow.mapNotNull
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
@@ -65,6 +84,7 @@ import eu.kanade.domain.manga.model.Manga as DomainManga
 | 
			
		||||
open class BrowseSourcePresenter(
 | 
			
		||||
    private val sourceId: Long,
 | 
			
		||||
    searchQuery: String? = null,
 | 
			
		||||
    private val state: BrowseSourceStateImpl = BrowseSourceState(searchQuery) as BrowseSourceStateImpl,
 | 
			
		||||
    private val sourceManager: SourceManager = Injekt.get(),
 | 
			
		||||
    private val preferences: PreferencesHelper = Injekt.get(),
 | 
			
		||||
    private val coverCache: CoverCache = Injekt.get(),
 | 
			
		||||
@@ -77,55 +97,76 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    private val updateManga: UpdateManga = Injekt.get(),
 | 
			
		||||
    private val insertTrack: InsertTrack = Injekt.get(),
 | 
			
		||||
    private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
 | 
			
		||||
) : BasePresenter<BrowseSourceController>() {
 | 
			
		||||
) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Selected source.
 | 
			
		||||
     */
 | 
			
		||||
    lateinit var source: CatalogueSource
 | 
			
		||||
    var displayMode by preferences.sourceDisplayMode().asState()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Modifiable list of filters.
 | 
			
		||||
     */
 | 
			
		||||
    var sourceFilters = FilterList()
 | 
			
		||||
        set(value) {
 | 
			
		||||
            field = value
 | 
			
		||||
            filterItems = value.toItems()
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getColumnsPreferenceForCurrentOrientation(): State<GridCells> {
 | 
			
		||||
        val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
 | 
			
		||||
        return produceState<GridCells>(initialValue = GridCells.Adaptive(128.dp), isLandscape) {
 | 
			
		||||
            (if (isLandscape) preferences.landscapeColumns() else preferences.portraitColumns())
 | 
			
		||||
                .asFlow()
 | 
			
		||||
                .collectLatest { columns ->
 | 
			
		||||
                    value = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var filterItems: List<IFlexible<*>> = emptyList()
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getMangaList(): Flow<PagingData<DomainManga>> {
 | 
			
		||||
        return remember(currentQuery, appliedFilters) {
 | 
			
		||||
            Pager(
 | 
			
		||||
                PagingConfig(pageSize = 25),
 | 
			
		||||
            ) {
 | 
			
		||||
                createPager(currentQuery, appliedFilters)
 | 
			
		||||
            }.flow
 | 
			
		||||
                .map {
 | 
			
		||||
                    it.map {
 | 
			
		||||
                        withIOContext {
 | 
			
		||||
                            networkToLocalManga(it, sourceId).toDomainManga()!!
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .cachedIn(presenterScope)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
 | 
			
		||||
     */
 | 
			
		||||
    var appliedFilters = FilterList()
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getManga(initialManga: DomainManga): State<DomainManga> {
 | 
			
		||||
        return produceState(initialValue = initialManga, initialManga.url, initialManga.source) {
 | 
			
		||||
            getManga.subscribe(initialManga.url, initialManga.source)
 | 
			
		||||
                .collectLatest { manga ->
 | 
			
		||||
                    if (manga == null) return@collectLatest
 | 
			
		||||
                    launchIO {
 | 
			
		||||
                        initializeMangas(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                    value = manga
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pager containing a list of manga results.
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var pager: Pager
 | 
			
		||||
    fun setFilter(filters: FilterList) {
 | 
			
		||||
        state.filters = filters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the pager.
 | 
			
		||||
     */
 | 
			
		||||
    private var pagerJob: Job? = null
 | 
			
		||||
    fun resetFilter() {
 | 
			
		||||
        state.appliedFilters = FilterList()
 | 
			
		||||
        val newFilters = source!!.getFilterList()
 | 
			
		||||
        state.filters = newFilters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for one request from the pager.
 | 
			
		||||
     */
 | 
			
		||||
    private var nextPageJob: Job? = null
 | 
			
		||||
    fun search() {
 | 
			
		||||
        state.currentQuery = searchQuery ?: ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        query = searchQuery ?: ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        source = sourceManager.get(sourceId) as? CatalogueSource ?: return
 | 
			
		||||
        sourceFilters = source.getFilterList()
 | 
			
		||||
        state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
 | 
			
		||||
        state.filters = source!!.getFilterList()
 | 
			
		||||
 | 
			
		||||
        if (savedState != null) {
 | 
			
		||||
            query = savedState.getString(::query.name, "")
 | 
			
		||||
@@ -137,79 +178,6 @@ open class BrowseSourcePresenter(
 | 
			
		||||
        super.onSave(state)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Restarts the pager for the active source with the provided query and filters.
 | 
			
		||||
     *
 | 
			
		||||
     * @param query the query.
 | 
			
		||||
     * @param filters the current state of the filters (for search mode).
 | 
			
		||||
     */
 | 
			
		||||
    fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
 | 
			
		||||
        this.query = query
 | 
			
		||||
        this.appliedFilters = filters
 | 
			
		||||
 | 
			
		||||
        // Create a new pager.
 | 
			
		||||
        pager = createPager(query, filters)
 | 
			
		||||
 | 
			
		||||
        val sourceId = source.id
 | 
			
		||||
        val sourceDisplayMode = preferences.sourceDisplayMode()
 | 
			
		||||
 | 
			
		||||
        pagerJob?.cancel()
 | 
			
		||||
        pagerJob = presenterScope.launchIO {
 | 
			
		||||
            pager.asFlow()
 | 
			
		||||
                .map { (first, second) ->
 | 
			
		||||
                    first to second.map {
 | 
			
		||||
                        networkToLocalManga(
 | 
			
		||||
                            it,
 | 
			
		||||
                            sourceId,
 | 
			
		||||
                        ).toDomainManga()!!
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .onEach { initializeMangas(it.second) }
 | 
			
		||||
                .map { (first, second) ->
 | 
			
		||||
                    first to second.map {
 | 
			
		||||
                        SourceItem(
 | 
			
		||||
                            it,
 | 
			
		||||
                            sourceDisplayMode,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .catch { error ->
 | 
			
		||||
                    logcat(LogPriority.ERROR, error)
 | 
			
		||||
                }
 | 
			
		||||
                .collectLatest { (page, mangas) ->
 | 
			
		||||
                    withUIContext {
 | 
			
		||||
                        view?.onAddPage(page, mangas)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Request first page.
 | 
			
		||||
        requestNext()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Requests the next page for the active pager.
 | 
			
		||||
     */
 | 
			
		||||
    fun requestNext() {
 | 
			
		||||
        if (!hasNextPage()) return
 | 
			
		||||
 | 
			
		||||
        nextPageJob?.cancel()
 | 
			
		||||
        nextPageJob = presenterScope.launchIO {
 | 
			
		||||
            try {
 | 
			
		||||
                pager.requestNextPage()
 | 
			
		||||
            } catch (e: Throwable) {
 | 
			
		||||
                withUIContext { view?.onAddPageError(e) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if the last fetched page has a next page.
 | 
			
		||||
     */
 | 
			
		||||
    fun hasNextPage(): Boolean {
 | 
			
		||||
        return pager.hasNextPage
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a manga from the database for the given manga from network. It creates a new entry
 | 
			
		||||
     * if the manga is not yet in the database.
 | 
			
		||||
@@ -217,16 +185,14 @@ open class BrowseSourcePresenter(
 | 
			
		||||
     * @param sManga the manga from the source.
 | 
			
		||||
     * @return a manga from the database.
 | 
			
		||||
     */
 | 
			
		||||
    private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
 | 
			
		||||
        var localManga = runBlocking { getManga.await(sManga.url, sourceId) }
 | 
			
		||||
    private suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
 | 
			
		||||
        var localManga = getManga.await(sManga.url, sourceId)
 | 
			
		||||
        if (localManga == null) {
 | 
			
		||||
            val newManga = Manga.create(sManga.url, sManga.title, sourceId)
 | 
			
		||||
            newManga.copyFrom(sManga)
 | 
			
		||||
            newManga.id = -1
 | 
			
		||||
            val result = runBlocking {
 | 
			
		||||
                val id = insertManga.await(newManga.toDomainManga()!!)
 | 
			
		||||
                getManga.await(id!!)
 | 
			
		||||
            }
 | 
			
		||||
            val id = insertManga.await(newManga.toDomainManga()!!)
 | 
			
		||||
            val result = getManga.await(id!!)
 | 
			
		||||
            localManga = result
 | 
			
		||||
        } else if (!localManga.favorite) {
 | 
			
		||||
            // if the manga isn't a favorite, set its display title from source
 | 
			
		||||
@@ -237,146 +203,123 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize a list of manga.
 | 
			
		||||
     * Initialize a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mangas the list of manga to initialize.
 | 
			
		||||
     */
 | 
			
		||||
    fun initializeMangas(mangas: List<DomainManga>) {
 | 
			
		||||
        presenterScope.launchIO {
 | 
			
		||||
            mangas.asFlow()
 | 
			
		||||
                .filter { it.thumbnailUrl == null && !it.initialized }
 | 
			
		||||
                .map { getMangaDetails(it.toDbManga()) }
 | 
			
		||||
                .onEach {
 | 
			
		||||
                    withUIContext {
 | 
			
		||||
                        @Suppress("DEPRECATION")
 | 
			
		||||
                        view?.onMangaInitialized(it.toDomainManga()!!)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .catch { e -> logcat(LogPriority.ERROR, e) }
 | 
			
		||||
                .collect()
 | 
			
		||||
    private suspend fun initializeMangas(manga: DomainManga) {
 | 
			
		||||
        if (manga.thumbnailUrl != null && manga.initialized) return
 | 
			
		||||
        withContext(NonCancellable) {
 | 
			
		||||
            val db = manga.toDbManga()
 | 
			
		||||
            try {
 | 
			
		||||
                val networkManga = source!!.getMangaDetails(db.copy())
 | 
			
		||||
                db.copyFrom(networkManga)
 | 
			
		||||
                db.initialized = true
 | 
			
		||||
                updateManga.await(
 | 
			
		||||
                    db
 | 
			
		||||
                        .toDomainManga()
 | 
			
		||||
                        ?.toMangaUpdate()!!,
 | 
			
		||||
                )
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                logcat(LogPriority.ERROR, e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the initialized manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to initialize.
 | 
			
		||||
     * @return the initialized manga
 | 
			
		||||
     */
 | 
			
		||||
    private suspend fun getMangaDetails(manga: Manga): Manga {
 | 
			
		||||
        try {
 | 
			
		||||
            val networkManga = source.getMangaDetails(manga.copy())
 | 
			
		||||
            manga.copyFrom(networkManga)
 | 
			
		||||
            manga.initialized = true
 | 
			
		||||
            updateManga.await(
 | 
			
		||||
                manga
 | 
			
		||||
                    .toDomainManga()
 | 
			
		||||
                    ?.toMangaUpdate()!!,
 | 
			
		||||
            )
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            logcat(LogPriority.ERROR, e)
 | 
			
		||||
        }
 | 
			
		||||
        return manga
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds or removes a manga from the library.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to update.
 | 
			
		||||
     */
 | 
			
		||||
    fun changeMangaFavorite(manga: Manga) {
 | 
			
		||||
        manga.favorite = !manga.favorite
 | 
			
		||||
        manga.date_added = when (manga.favorite) {
 | 
			
		||||
            true -> Date().time
 | 
			
		||||
            false -> 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!manga.favorite) {
 | 
			
		||||
            manga.removeCovers(coverCache)
 | 
			
		||||
        } else {
 | 
			
		||||
            ChapterSettingsHelper.applySettingDefaults(manga.toDomainManga()!!)
 | 
			
		||||
 | 
			
		||||
            autoAddTrack(manga)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            updateManga.await(
 | 
			
		||||
                manga
 | 
			
		||||
                    .toDomainManga()
 | 
			
		||||
                    ?.toMangaUpdate()!!,
 | 
			
		||||
    fun changeMangaFavorite(manga: DomainManga) {
 | 
			
		||||
        presenterScope.launch {
 | 
			
		||||
            var new = manga.copy(
 | 
			
		||||
                favorite = !manga.favorite,
 | 
			
		||||
                dateAdded = when (manga.favorite) {
 | 
			
		||||
                    true -> Date().time
 | 
			
		||||
                    false -> 0
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (!new.favorite) {
 | 
			
		||||
                new = new.removeCovers(coverCache)
 | 
			
		||||
            } else {
 | 
			
		||||
                ChapterSettingsHelper.applySettingDefaults(manga)
 | 
			
		||||
 | 
			
		||||
                autoAddTrack(manga)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            updateManga.await(new.toMangaUpdate())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun autoAddTrack(manga: Manga) {
 | 
			
		||||
        launchIO {
 | 
			
		||||
            loggedServices
 | 
			
		||||
                .filterIsInstance<EnhancedTrackService>()
 | 
			
		||||
                .filter { it.accept(source) }
 | 
			
		||||
                .forEach { service ->
 | 
			
		||||
                    try {
 | 
			
		||||
                        service.match(manga)?.let { track ->
 | 
			
		||||
                            track.manga_id = manga.id!!
 | 
			
		||||
                            (service as TrackService).bind(track)
 | 
			
		||||
                            insertTrack.await(track.toDomainTrack()!!)
 | 
			
		||||
    fun getSourceOrStub(manga: DomainManga): Source {
 | 
			
		||||
        return sourceManager.getOrStub(manga.source)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
                            val chapters = getChapterByMangaId.await(manga.id!!)
 | 
			
		||||
                            syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service)
 | 
			
		||||
                        }
 | 
			
		||||
                    } catch (e: Exception) {
 | 
			
		||||
                        logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" }
 | 
			
		||||
                    }
 | 
			
		||||
    fun addFavorite(manga: DomainManga) {
 | 
			
		||||
        presenterScope.launch {
 | 
			
		||||
            val categories = getCategories()
 | 
			
		||||
            val defaultCategoryId = preferences.defaultCategory()
 | 
			
		||||
            val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
 | 
			
		||||
 | 
			
		||||
            when {
 | 
			
		||||
                // Default category set
 | 
			
		||||
                defaultCategory != null -> {
 | 
			
		||||
                    moveMangaToCategories(manga, defaultCategory)
 | 
			
		||||
 | 
			
		||||
                    changeMangaFavorite(manga)
 | 
			
		||||
                    // activity.toast(activity.getString(R.string.manga_added_library))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Automatic 'Default' or no categories
 | 
			
		||||
                defaultCategoryId == 0 || categories.isEmpty() -> {
 | 
			
		||||
                    moveMangaToCategories(manga)
 | 
			
		||||
 | 
			
		||||
                    changeMangaFavorite(manga)
 | 
			
		||||
                    // activity.toast(activity.getString(R.string.manga_added_library))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Choose a category
 | 
			
		||||
                else -> {
 | 
			
		||||
                    val preselectedIds = getCategories.await(manga.id).map { it.id }
 | 
			
		||||
                    state.dialog = Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun autoAddTrack(manga: DomainManga) {
 | 
			
		||||
        loggedServices
 | 
			
		||||
            .filterIsInstance<EnhancedTrackService>()
 | 
			
		||||
            .filter { it.accept(source!!) }
 | 
			
		||||
            .forEach { service ->
 | 
			
		||||
                try {
 | 
			
		||||
                    service.match(manga.toDbManga())?.let { track ->
 | 
			
		||||
                        track.manga_id = manga.id
 | 
			
		||||
                        (service as TrackService).bind(track)
 | 
			
		||||
                        insertTrack.await(track.toDomainTrack()!!)
 | 
			
		||||
 | 
			
		||||
                        val chapters = getChapterByMangaId.await(manga.id)
 | 
			
		||||
                        syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service)
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the filter states for the current source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param filters a list of active filters.
 | 
			
		||||
     */
 | 
			
		||||
    fun setSourceFilter(filters: FilterList) {
 | 
			
		||||
        restartPager(filters = filters)
 | 
			
		||||
        state.appliedFilters = filters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun createPager(query: String, filters: FilterList): Pager {
 | 
			
		||||
        return SourcePager(source, query, filters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun FilterList.toItems(): List<IFlexible<*>> {
 | 
			
		||||
        return mapNotNull { filter ->
 | 
			
		||||
            when (filter) {
 | 
			
		||||
                is Filter.Header -> HeaderItem(filter)
 | 
			
		||||
                is Filter.Separator -> SeparatorItem(filter)
 | 
			
		||||
                is Filter.CheckBox -> CheckboxItem(filter)
 | 
			
		||||
                is Filter.TriState -> TriStateItem(filter)
 | 
			
		||||
                is Filter.Text -> TextItem(filter)
 | 
			
		||||
                is Filter.Select<*> -> SelectItem(filter)
 | 
			
		||||
                is Filter.Group<*> -> {
 | 
			
		||||
                    val group = GroupItem(filter)
 | 
			
		||||
                    val subItems = filter.state.mapNotNull {
 | 
			
		||||
                        when (it) {
 | 
			
		||||
                            is Filter.CheckBox -> CheckboxSectionItem(it)
 | 
			
		||||
                            is Filter.TriState -> TriStateSectionItem(it)
 | 
			
		||||
                            is Filter.Text -> TextSectionItem(it)
 | 
			
		||||
                            is Filter.Select<*> -> SelectSectionItem(it)
 | 
			
		||||
                            else -> null
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    subItems.forEach { it.header = group }
 | 
			
		||||
                    group.subItems = subItems
 | 
			
		||||
                    group
 | 
			
		||||
                }
 | 
			
		||||
                is Filter.Sort -> {
 | 
			
		||||
                    val group = SortGroup(filter)
 | 
			
		||||
                    val subItems = filter.values.map {
 | 
			
		||||
                        SortItem(it, group)
 | 
			
		||||
                    }
 | 
			
		||||
                    group.subItems = subItems
 | 
			
		||||
                    group
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    open fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
 | 
			
		||||
        return SourceBrowsePagingSource(source!!, query, filters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -395,54 +338,67 @@ open class BrowseSourcePresenter(
 | 
			
		||||
        return getDuplicateLibraryManga.await(manga.title, manga.source)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to get categories from.
 | 
			
		||||
     * @return Array of category ids the manga is in, if none returns default id
 | 
			
		||||
     */
 | 
			
		||||
    fun getMangaCategoryIds(manga: DomainManga): Array<Long?> {
 | 
			
		||||
        return runBlocking { getCategories.await(manga.id) }
 | 
			
		||||
            .map { it.id }
 | 
			
		||||
            .toTypedArray()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Move the given manga to categories.
 | 
			
		||||
     *
 | 
			
		||||
     * @param categories the selected categories.
 | 
			
		||||
     * @param manga the manga to move.
 | 
			
		||||
     */
 | 
			
		||||
    private fun moveMangaToCategories(manga: Manga, categories: List<DomainCategory>) {
 | 
			
		||||
    fun moveMangaToCategories(manga: DomainManga, vararg categories: DomainCategory) {
 | 
			
		||||
        moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun moveMangaToCategories(manga: DomainManga, categoryIds: List<Long>) {
 | 
			
		||||
        presenterScope.launchIO {
 | 
			
		||||
            setMangaCategories.await(
 | 
			
		||||
                mangaId = manga.id!!,
 | 
			
		||||
                categoryIds = categories.filter { it.id != 0L }.map { it.id },
 | 
			
		||||
                mangaId = manga.id,
 | 
			
		||||
                categoryIds = categoryIds.toList(),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Move the given manga to the category.
 | 
			
		||||
     *
 | 
			
		||||
     * @param category the selected category.
 | 
			
		||||
     * @param manga the manga to move.
 | 
			
		||||
     */
 | 
			
		||||
    fun moveMangaToCategory(manga: Manga, category: DomainCategory?) {
 | 
			
		||||
        moveMangaToCategories(manga, listOfNotNull(category))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update manga to use selected categories.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga needed to change
 | 
			
		||||
     * @param selectedCategories selected categories
 | 
			
		||||
     */
 | 
			
		||||
    fun updateMangaCategories(manga: Manga, selectedCategories: List<DomainCategory>) {
 | 
			
		||||
        if (!manga.favorite) {
 | 
			
		||||
            changeMangaFavorite(manga)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        moveMangaToCategories(manga, selectedCategories)
 | 
			
		||||
    sealed class Dialog {
 | 
			
		||||
        data class RemoveManga(val manga: DomainManga) : Dialog()
 | 
			
		||||
        data class AddDuplicateManga(val manga: DomainManga, val duplicate: DomainManga) : Dialog()
 | 
			
		||||
        data class ChangeMangaCategory(
 | 
			
		||||
            val manga: DomainManga,
 | 
			
		||||
            val initialSelection: List<CheckboxState.State<DomainCategory>>,
 | 
			
		||||
        ) : Dialog()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun FilterList.toItems(): List<IFlexible<*>> {
 | 
			
		||||
    return mapNotNull { filter ->
 | 
			
		||||
        when (filter) {
 | 
			
		||||
            is Filter.Header -> HeaderItem(filter)
 | 
			
		||||
            is Filter.Separator -> SeparatorItem(filter)
 | 
			
		||||
            is Filter.CheckBox -> CheckboxItem(filter)
 | 
			
		||||
            is Filter.TriState -> TriStateItem(filter)
 | 
			
		||||
            is Filter.Text -> TextItem(filter)
 | 
			
		||||
            is Filter.Select<*> -> SelectItem(filter)
 | 
			
		||||
            is Filter.Group<*> -> {
 | 
			
		||||
                val group = GroupItem(filter)
 | 
			
		||||
                val subItems = filter.state.mapNotNull {
 | 
			
		||||
                    when (it) {
 | 
			
		||||
                        is Filter.CheckBox -> CheckboxSectionItem(it)
 | 
			
		||||
                        is Filter.TriState -> TriStateSectionItem(it)
 | 
			
		||||
                        is Filter.Text -> TextSectionItem(it)
 | 
			
		||||
                        is Filter.Select<*> -> SelectSectionItem(it)
 | 
			
		||||
                        else -> null
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                subItems.forEach { it.header = group }
 | 
			
		||||
                group.subItems = subItems
 | 
			
		||||
                group
 | 
			
		||||
            }
 | 
			
		||||
            is Filter.Sort -> {
 | 
			
		||||
                val group = SortGroup(filter)
 | 
			
		||||
                val subItems = filter.values.map {
 | 
			
		||||
                    SortItem(it, group)
 | 
			
		||||
                }
 | 
			
		||||
                group.subItems = subItems
 | 
			
		||||
                group
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import com.jakewharton.rxrelay.PublishRelay
 | 
			
		||||
import eu.kanade.core.util.asFlow
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A general pager for source requests (latest updates, popular, search)
 | 
			
		||||
 */
 | 
			
		||||
abstract class Pager(var currentPage: Int = 1) {
 | 
			
		||||
 | 
			
		||||
    var hasNextPage = true
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
 | 
			
		||||
 | 
			
		||||
    fun asFlow(): Flow<Pair<Int, List<SManga>>> {
 | 
			
		||||
        return results.asObservable().asFlow()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract suspend fun requestNextPage()
 | 
			
		||||
 | 
			
		||||
    fun onPageReceived(mangasPage: MangasPage) {
 | 
			
		||||
        val page = currentPage
 | 
			
		||||
        currentPage++
 | 
			
		||||
        hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
 | 
			
		||||
        results.call(Pair(page, mangasPage.mangas))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.ProgressBar
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
 | 
			
		||||
 | 
			
		||||
    private var loadMore = true
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.source_progress_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
 | 
			
		||||
        holder.progressBar.isVisible = false
 | 
			
		||||
        holder.progressMessage.isVisible = false
 | 
			
		||||
 | 
			
		||||
        if (!adapter.isEndlessScrollEnabled) {
 | 
			
		||||
            loadMore = false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (loadMore) {
 | 
			
		||||
            holder.progressBar.isVisible = true
 | 
			
		||||
        } else {
 | 
			
		||||
            holder.progressMessage.isVisible = true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        return this === other
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return loadMore.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
 | 
			
		||||
 | 
			
		||||
        val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
 | 
			
		||||
        val progressMessage: TextView = view.findViewById(R.id.progress_message)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
 | 
			
		||||
class SourceBrowsePagingSource(val source: CatalogueSource, val query: String, val filters: FilterList) : BrowsePagingSource() {
 | 
			
		||||
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        val observable = if (query.isBlank() && filters.isEmpty()) {
 | 
			
		||||
            source.fetchPopularManga(currentPage)
 | 
			
		||||
        } else {
 | 
			
		||||
            source.fetchSearchManga(currentPage, query, filters)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return observable.awaitSingle()
 | 
			
		||||
            .takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
 | 
			
		||||
 * All the elements from the layout file "item_source_grid" are available in this class.
 | 
			
		||||
 *
 | 
			
		||||
 * @param binding the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 * @constructor creates a new catalogue holder.
 | 
			
		||||
 */
 | 
			
		||||
class SourceComfortableGridHolder(
 | 
			
		||||
    override val binding: SourceComfortableGridItemBinding,
 | 
			
		||||
    adapter: FlexibleAdapter<*>,
 | 
			
		||||
) : SourceHolder<SourceComfortableGridItemBinding>(binding.root, adapter) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
 | 
			
		||||
     * holder with the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onSetValues(manga: Manga) {
 | 
			
		||||
        // Set manga title
 | 
			
		||||
        binding.title.text = manga.title
 | 
			
		||||
 | 
			
		||||
        // Set alpha of thumbnail.
 | 
			
		||||
        binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
 | 
			
		||||
 | 
			
		||||
        // For rounded corners
 | 
			
		||||
        binding.badges.leftBadges.clipToOutline = true
 | 
			
		||||
        binding.badges.rightBadges.clipToOutline = true
 | 
			
		||||
 | 
			
		||||
        // Set favorite badge
 | 
			
		||||
        binding.badges.favoriteText.isVisible = manga.favorite
 | 
			
		||||
 | 
			
		||||
        setImage(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setImage(manga: Manga) {
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        binding.thumbnail.loadAutoPause(manga) {
 | 
			
		||||
            setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
 | 
			
		||||
 * All the elements from the layout file "item_source_grid" are available in this class.
 | 
			
		||||
 *
 | 
			
		||||
 * @param binding the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 * @constructor creates a new catalogue holder.
 | 
			
		||||
 */
 | 
			
		||||
class SourceCompactGridHolder(
 | 
			
		||||
    override val binding: SourceCompactGridItemBinding,
 | 
			
		||||
    adapter: FlexibleAdapter<*>,
 | 
			
		||||
) : SourceHolder<SourceCompactGridItemBinding>(binding.root, adapter) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
 | 
			
		||||
     * holder with the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onSetValues(manga: Manga) {
 | 
			
		||||
        // Set manga title
 | 
			
		||||
        binding.title.text = manga.title
 | 
			
		||||
 | 
			
		||||
        // Set alpha of thumbnail.
 | 
			
		||||
        binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
 | 
			
		||||
 | 
			
		||||
        // For rounded corners
 | 
			
		||||
        binding.badges.leftBadges.clipToOutline = true
 | 
			
		||||
        binding.badges.rightBadges.clipToOutline = true
 | 
			
		||||
 | 
			
		||||
        // Set favorite badge
 | 
			
		||||
        binding.badges.favoriteText.isVisible = manga.favorite
 | 
			
		||||
 | 
			
		||||
        setImage(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setImage(manga: Manga) {
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        binding.thumbnail.loadAutoPause(manga) {
 | 
			
		||||
            setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.viewbinding.ViewBinding
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generic class used to hold the displayed data of a manga in the catalogue.
 | 
			
		||||
 *
 | 
			
		||||
 * @param view the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 */
 | 
			
		||||
abstract class SourceHolder<VB : ViewBinding>(view: View, adapter: FlexibleAdapter<*>) :
 | 
			
		||||
    FlexibleViewHolder(view, adapter) {
 | 
			
		||||
 | 
			
		||||
    abstract val binding: VB
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
 | 
			
		||||
     * holder with the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    abstract fun onSetValues(manga: Manga)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates the image for this holder. Useful to update the image when the manga is initialized
 | 
			
		||||
     * and the url is now known.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    abstract fun setImage(manga: Manga)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.fredporciuncula.flow.preferences.Preference
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
 | 
			
		||||
class SourceItem(val manga: Manga, private val displayMode: Preference<LibraryDisplayMode>) :
 | 
			
		||||
    AbstractFlexibleItem<SourceHolder<*>>() {
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return when (displayMode.get()) {
 | 
			
		||||
            LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> R.layout.source_compact_grid_item
 | 
			
		||||
            LibraryDisplayMode.ComfortableGrid -> R.layout.source_comfortable_grid_item
 | 
			
		||||
            LibraryDisplayMode.List -> R.layout.source_list_item
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(
 | 
			
		||||
        view: View,
 | 
			
		||||
        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
    ): SourceHolder<*> {
 | 
			
		||||
        return when (displayMode.get()) {
 | 
			
		||||
            LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
 | 
			
		||||
                SourceCompactGridHolder(SourceCompactGridItemBinding.bind(view), adapter)
 | 
			
		||||
            }
 | 
			
		||||
            LibraryDisplayMode.ComfortableGrid -> {
 | 
			
		||||
                SourceComfortableGridHolder(SourceComfortableGridItemBinding.bind(view), adapter)
 | 
			
		||||
            }
 | 
			
		||||
            LibraryDisplayMode.List -> {
 | 
			
		||||
                SourceListHolder(view, adapter)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(
 | 
			
		||||
        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
        holder: SourceHolder<*>,
 | 
			
		||||
        position: Int,
 | 
			
		||||
        payloads: List<Any?>?,
 | 
			
		||||
    ) {
 | 
			
		||||
        holder.onSetValues(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other is SourceItem) {
 | 
			
		||||
            return manga.id == other.manga.id
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return manga.id.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getResourceColor
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
 | 
			
		||||
 * All the elements from the layout file "item_catalogue_list" are available in this class.
 | 
			
		||||
 *
 | 
			
		||||
 * @param view the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 * @constructor creates a new catalogue holder.
 | 
			
		||||
 */
 | 
			
		||||
class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
 | 
			
		||||
    SourceHolder<SourceListItemBinding>(view, adapter) {
 | 
			
		||||
 | 
			
		||||
    override val binding = SourceListItemBinding.bind(view)
 | 
			
		||||
 | 
			
		||||
    private val favoriteColor = view.context.getResourceColor(R.attr.colorOnSurface, 0.38f)
 | 
			
		||||
    private val unfavoriteColor = view.context.getResourceColor(R.attr.colorOnSurface)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
 | 
			
		||||
     * holder with the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onSetValues(manga: Manga) {
 | 
			
		||||
        binding.title.text = manga.title
 | 
			
		||||
        binding.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
 | 
			
		||||
 | 
			
		||||
        // Set alpha of thumbnail.
 | 
			
		||||
        binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
 | 
			
		||||
 | 
			
		||||
        // For rounded corners
 | 
			
		||||
        binding.badges.clipToOutline = true
 | 
			
		||||
 | 
			
		||||
        // Set favorite badge
 | 
			
		||||
        binding.favoriteText.isVisible = manga.favorite
 | 
			
		||||
 | 
			
		||||
        setImage(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setImage(manga: Manga) {
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        if (!manga.thumbnailUrl.isNullOrEmpty()) {
 | 
			
		||||
            binding.thumbnail.load(manga) {
 | 
			
		||||
                setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
 | 
			
		||||
class SourcePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() {
 | 
			
		||||
 | 
			
		||||
    override suspend fun requestNextPage() {
 | 
			
		||||
        val page = currentPage
 | 
			
		||||
 | 
			
		||||
        val observable = if (query.isBlank() && filters.isEmpty()) {
 | 
			
		||||
            source.fetchPopularManga(page)
 | 
			
		||||
        } else {
 | 
			
		||||
            source.fetchSearchManga(page, query, filters)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val mangasPage = observable.awaitSingle()
 | 
			
		||||
 | 
			
		||||
        if (mangasPage.mangas.isNotEmpty()) {
 | 
			
		||||
            onPageReceived(mangasPage)
 | 
			
		||||
        } else {
 | 
			
		||||
            throw NoResultsException()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesBrowsePagingSource(val source: CatalogueSource) : BrowsePagingSource() {
 | 
			
		||||
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        return source.fetchLatestUpdates(currentPage).awaitSingle()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,20 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.core.os.bundleOf
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseLatestScreen
 | 
			
		||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchIO
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
 | 
			
		||||
@@ -21,9 +29,63 @@ class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) {
 | 
			
		||||
        return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        super.onPrepareOptionsMenu(menu)
 | 
			
		||||
        menu.findItem(R.id.action_search).isVisible = false
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
 | 
			
		||||
        BrowseLatestScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onMangaClick = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
            onMangaLongClick = { manga ->
 | 
			
		||||
                scope.launchIO {
 | 
			
		||||
                    val duplicateManga = presenter.getDuplicateLibraryManga(manga)
 | 
			
		||||
                    when {
 | 
			
		||||
                        manga.favorite -> presenter.dialog = BrowseSourcePresenter.Dialog.RemoveManga(manga)
 | 
			
		||||
                        duplicateManga != null -> presenter.dialog = BrowseSourcePresenter.Dialog.AddDuplicateManga(manga, duplicateManga)
 | 
			
		||||
                        else -> presenter.addFavorite(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val onDismissRequest = { presenter.dialog = null }
 | 
			
		||||
        when (val dialog = presenter.dialog) {
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.AddDuplicateManga -> {
 | 
			
		||||
                DuplicateMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onOpenManga = {
 | 
			
		||||
                        router.pushController(MangaController(dialog.duplicate.id, true))
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        presenter.addFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                    duplicateFrom = presenter.getSourceOrStub(dialog.manga),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.RemoveManga -> {
 | 
			
		||||
                RemoveMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.ChangeMangaCategory -> {
 | 
			
		||||
                ChangeCategoryDialog(
 | 
			
		||||
                    initialSelection = dialog.initialSelection,
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onEditCategories = {
 | 
			
		||||
                        router.pushController(CategoryController())
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = { include, _ ->
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                        presenter.moveMangaToCategories(dialog.manga, include)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            null -> {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun initFilterSheet() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesPager(val source: CatalogueSource) : Pager() {
 | 
			
		||||
 | 
			
		||||
    override suspend fun requestNextPage() {
 | 
			
		||||
        val mangasPage = source.fetchLatestUpdates(currentPage).awaitSingle()
 | 
			
		||||
        onPageReceived(mangasPage)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
 | 
			
		||||
 | 
			
		||||
    override fun createPager(query: String, filters: FilterList): Pager {
 | 
			
		||||
        return LatestUpdatesPager(source)
 | 
			
		||||
    override fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
 | 
			
		||||
        return LatestUpdatesBrowsePagingSource(source!!)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import com.bluelinelabs.conductor.Controller
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import eu.kanade.domain.category.model.Category
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.category.visualName
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryController
 | 
			
		||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
 | 
			
		||||
import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
 | 
			
		||||
 | 
			
		||||
class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
 | 
			
		||||
    DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    private var mangas = emptyList<Manga>()
 | 
			
		||||
    private var categories = emptyList<Category>()
 | 
			
		||||
    private var preselected = emptyArray<Int>()
 | 
			
		||||
    private var selected = emptyArray<Int>().toIntArray()
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        target: T,
 | 
			
		||||
        mangas: List<Manga>,
 | 
			
		||||
        categories: List<Category>,
 | 
			
		||||
        preselected: Array<Int>,
 | 
			
		||||
    ) : this() {
 | 
			
		||||
        this.mangas = mangas
 | 
			
		||||
        this.categories = categories
 | 
			
		||||
        this.preselected = preselected
 | 
			
		||||
        this.selected = preselected.toIntArray()
 | 
			
		||||
        targetController = target
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
 | 
			
		||||
        return MaterialAlertDialogBuilder(activity!!)
 | 
			
		||||
            .setTitle(R.string.action_move_category)
 | 
			
		||||
            .setNegativeButton(android.R.string.cancel, null)
 | 
			
		||||
            .apply {
 | 
			
		||||
                if (categories.isNotEmpty()) {
 | 
			
		||||
                    setQuadStateMultiChoiceItems(
 | 
			
		||||
                        items = categories.map { it.visualName(context) },
 | 
			
		||||
                        isActionList = false,
 | 
			
		||||
                        initialSelected = preselected.toIntArray(),
 | 
			
		||||
                    ) { selections ->
 | 
			
		||||
                        selected = selections
 | 
			
		||||
                    }
 | 
			
		||||
                    setPositiveButton(android.R.string.ok) { _, _ ->
 | 
			
		||||
                        val add = selected
 | 
			
		||||
                            .mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) categories[index] else null }
 | 
			
		||||
                            .filterNotNull()
 | 
			
		||||
                        val remove = selected
 | 
			
		||||
                            .mapIndexed { index, value -> if (value == QuadStateTextView.State.UNCHECKED.ordinal) categories[index] else null }
 | 
			
		||||
                            .filterNotNull()
 | 
			
		||||
                        (targetController as? Listener)?.updateCategoriesForMangas(mangas, add, remove)
 | 
			
		||||
                    }
 | 
			
		||||
                    setNeutralButton(R.string.action_edit) { _, _ -> openCategoryController() }
 | 
			
		||||
                } else {
 | 
			
		||||
                    setMessage(R.string.information_empty_category_dialog)
 | 
			
		||||
                    setPositiveButton(R.string.action_edit_categories) { _, _ -> openCategoryController() }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .create()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openCategoryController() {
 | 
			
		||||
        if (targetController is LibraryController) {
 | 
			
		||||
            val libController = targetController as LibraryController
 | 
			
		||||
            libController.clearSelection()
 | 
			
		||||
        }
 | 
			
		||||
        router.popCurrentController()
 | 
			
		||||
        router.pushController(CategoryController())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface Listener {
 | 
			
		||||
        fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category> = emptyList<Category>())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import com.bluelinelabs.conductor.Controller
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) {
 | 
			
		||||
 | 
			
		||||
    private val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private lateinit var libraryManga: Manga
 | 
			
		||||
    private lateinit var onAddToLibrary: () -> Unit
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        target: Controller,
 | 
			
		||||
        libraryManga: Manga,
 | 
			
		||||
        onAddToLibrary: () -> Unit,
 | 
			
		||||
    ) : this() {
 | 
			
		||||
        targetController = target
 | 
			
		||||
 | 
			
		||||
        this.libraryManga = libraryManga
 | 
			
		||||
        this.onAddToLibrary = onAddToLibrary
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
 | 
			
		||||
        val source = sourceManager.getOrStub(libraryManga.source)
 | 
			
		||||
 | 
			
		||||
        return MaterialAlertDialogBuilder(activity!!)
 | 
			
		||||
            .setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
 | 
			
		||||
            .setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
 | 
			
		||||
                onAddToLibrary()
 | 
			
		||||
            }
 | 
			
		||||
            .setNegativeButton(android.R.string.cancel, null)
 | 
			
		||||
            .setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
 | 
			
		||||
                dismissDialog()
 | 
			
		||||
                router.pushController(MangaController(libraryManga.id))
 | 
			
		||||
            }
 | 
			
		||||
            .setCancelable(true)
 | 
			
		||||
            .create()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,12 +22,12 @@ import eu.kanade.data.chapter.NoChaptersException
 | 
			
		||||
import eu.kanade.domain.manga.model.toDbManga
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.LoadingScreen
 | 
			
		||||
import eu.kanade.presentation.manga.DownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.MangaScreen
 | 
			
		||||
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,12 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int {
 | 
			
		||||
    return coverCache.deleteFromCache(this, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun DomainManga.removeCovers(coverCache: CoverCache = Injekt.get()): DomainManga {
 | 
			
		||||
    if (isLocal()) return this
 | 
			
		||||
    coverCache.deleteFromCache(this, true)
 | 
			
		||||
    return copy(coverLastModified = Date().time)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun DomainManga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: PreferencesHelper): Boolean {
 | 
			
		||||
    if (!favorite) return false
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user