mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-27 03:18:25 +01:00
Use Compose on BrowseSourceScreens (#7901)
This commit is contained in:
parent
bb54a81ef0
commit
d4b764fa31
@ -27,6 +27,10 @@ class MangaRepositoryImpl(
|
|||||||
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
|
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> {
|
override suspend fun getFavorites(): List<Manga> {
|
||||||
return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
|
return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
|
||||||
}
|
}
|
||||||
|
@ -26,4 +26,8 @@ class GetManga(
|
|||||||
suspend fun await(url: String, sourceId: Long): Manga? {
|
suspend fun await(url: String, sourceId: Long): Manga? {
|
||||||
return mangaRepository.getMangaByUrlAndSourceId(url, sourceId)
|
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?
|
suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga?
|
||||||
|
|
||||||
|
fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?>
|
||||||
|
|
||||||
suspend fun getFavorites(): List<Manga>
|
suspend fun getFavorites(): List<Manga>
|
||||||
|
|
||||||
suspend fun getLibraryManga(): List<LibraryManga>
|
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.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
@ -78,13 +78,22 @@ fun LibraryComfortableGridItem(
|
|||||||
isLocal = item.isLocal,
|
isLocal = item.isLocal,
|
||||||
language = item.sourceLanguage,
|
language = item.sourceLanguage,
|
||||||
)
|
)
|
||||||
Text(
|
MangaGridComfortableText(
|
||||||
modifier = Modifier.padding(4.dp),
|
|
||||||
text = manga.title,
|
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.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@ -97,20 +98,27 @@ fun LibraryCompactGridItem(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
)
|
)
|
||||||
Text(
|
MangaGridCompactText(manga.title)
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.presentation.components.MangaCover
|
||||||
import eu.kanade.tachiyomi.R
|
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
|
@Composable
|
||||||
fun LibraryGridCover(
|
fun LibraryGridCover(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@ -26,54 +62,41 @@ fun LibraryGridCover(
|
|||||||
language: String,
|
language: String,
|
||||||
content: @Composable BoxScope.() -> Unit = {},
|
content: @Composable BoxScope.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Box(
|
MangaGridCover(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
cover = {
|
||||||
.aspectRatio(MangaCover.Book.ratio),
|
MangaCover.Book(
|
||||||
) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
MangaCover.Book(
|
data = mangaCover,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
)
|
||||||
data = mangaCover,
|
},
|
||||||
)
|
badgesStart = {
|
||||||
content()
|
if (downloadCount > 0) {
|
||||||
if (downloadCount > 0 || unreadCount > 0) {
|
Badge(
|
||||||
BadgeGroup(
|
text = "$downloadCount",
|
||||||
modifier = Modifier
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
.padding(4.dp)
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
.align(Alignment.TopStart),
|
)
|
||||||
) {
|
|
||||||
if (downloadCount > 0) {
|
|
||||||
Badge(
|
|
||||||
text = "$downloadCount",
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
Badge(text = "$unreadCount")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (unreadCount > 0) {
|
||||||
if (isLocal || language.isNotEmpty()) {
|
Badge(text = "$unreadCount")
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
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.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
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.Badge
|
||||||
import eu.kanade.presentation.components.BadgeGroup
|
import eu.kanade.presentation.components.BadgeGroup
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
|
import eu.kanade.presentation.components.MangaCover.Square
|
||||||
import eu.kanade.presentation.components.TextButton
|
import eu.kanade.presentation.components.TextButton
|
||||||
import eu.kanade.presentation.util.bottomNavPaddingValues
|
import eu.kanade.presentation.util.bottomNavPaddingValues
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.horizontalPadding
|
||||||
@ -74,62 +76,109 @@ fun LibraryListItem(
|
|||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
) {
|
) {
|
||||||
val manga = item.manga
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.selectedBackground(isSelected)
|
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = { onClick(manga) },
|
onClick = onClick,
|
||||||
onLongClick = { onLongClick(manga) },
|
onLongClick = onLongClick,
|
||||||
)
|
)
|
||||||
.padding(horizontal = horizontalPadding),
|
.padding(horizontal = horizontalPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
eu.kanade.presentation.components.MangaCover.Square(
|
coverContent()
|
||||||
modifier = Modifier
|
content()
|
||||||
.padding(vertical = verticalPadding)
|
BadgeGroup(content = badges)
|
||||||
.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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import eu.kanade.domain.manga.model.Manga as DomainManga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create cover cache.
|
* Class used to create cover cache.
|
||||||
@ -87,6 +88,20 @@ class CoverCache(private val context: Context) {
|
|||||||
return deleted
|
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
|
* Delete custom cover of the manga from the cache
|
||||||
*
|
*
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import android.os.Bundle
|
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.domain.manga.model.Manga
|
||||||
|
import eu.kanade.presentation.browse.SourceSearchScreen
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
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
|
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||||
|
|
||||||
class SourceSearchController(
|
class SourceSearchController(
|
||||||
@ -13,30 +15,34 @@ class SourceSearchController(
|
|||||||
) : BrowseSourceController(bundle) {
|
) : BrowseSourceController(bundle) {
|
||||||
|
|
||||||
constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
|
constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
|
||||||
Bundle().apply {
|
bundleOf(
|
||||||
putLong(SOURCE_ID_KEY, source.id)
|
SOURCE_ID_KEY to source.id,
|
||||||
putSerializable(MANGA_KEY, manga)
|
MANGA_KEY to manga,
|
||||||
if (searchQuery != null) {
|
SEARCH_QUERY_KEY to searchQuery,
|
||||||
putString(SEARCH_QUERY_KEY, searchQuery)
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private var oldManga: Manga? = args.getSerializableCompat(MANGA_KEY)
|
private var oldManga: Manga? = args.getSerializableCompat(MANGA_KEY)
|
||||||
private var newManga: Manga? = null
|
private var newManga: Manga? = null
|
||||||
|
|
||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
@Composable
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
override fun ComposeContent() {
|
||||||
newManga = item.manga
|
SourceSearchScreen(
|
||||||
val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
|
presenter = presenter,
|
||||||
val dialog =
|
navigateUp = { router.popCurrentController() },
|
||||||
SearchController.MigrationDialog(oldManga, newManga, this)
|
onFabClick = { filterSheet?.show() },
|
||||||
dialog.targetController = searchController
|
onClickManga = {
|
||||||
dialog.showDialog(router)
|
newManga = it
|
||||||
return true
|
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) {
|
LaunchedEffect(presenter.filters) {
|
||||||
view?.let { super.onItemClick(it, position) }
|
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
|
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.Menu
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import android.view.MenuInflater
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import android.view.MenuItem
|
import androidx.core.os.bundleOf
|
||||||
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 eu.kanade.domain.source.model.Source
|
import eu.kanade.domain.source.model.Source
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.presentation.browse.BrowseSourceScreen
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||||
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||||
|
import eu.kanade.presentation.components.DuplicateMangaDialog
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||||
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.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Dialog
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||||
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.manga.MangaController
|
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.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) :
|
open class BrowseSourceController(bundle: Bundle) :
|
||||||
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
FullComposeController<BrowseSourcePresenter>(bundle) {
|
||||||
FabController,
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
|
||||||
FlexibleAdapter.EndlessScrollListener,
|
|
||||||
ChangeMangaCategoriesDialog.Listener {
|
|
||||||
|
|
||||||
constructor(sourceId: Long, query: String? = null) : this(
|
constructor(sourceId: Long, query: String? = null) : this(
|
||||||
Bundle().apply {
|
bundleOf(
|
||||||
putLong(SOURCE_ID_KEY, sourceId)
|
SOURCE_ID_KEY to sourceId,
|
||||||
query?.let { query ->
|
SEARCH_QUERY_KEY to query,
|
||||||
putString(SEARCH_QUERY_KEY, query)
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(source: CatalogueSource, query: String? = null) : this(source.id, query)
|
constructor(source: CatalogueSource, query: String? = null) : this(source.id, query)
|
||||||
|
|
||||||
constructor(source: Source, 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.
|
* Sheet containing filter items.
|
||||||
*/
|
*/
|
||||||
private var filterSheet: SourceFilterSheet? = null
|
protected var filterSheet: SourceFilterSheet? = null
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* Recycler view with the list of results.
|
override fun ComposeContent() {
|
||||||
*/
|
val scope = rememberCoroutineScope()
|
||||||
private var recycler: RecyclerView? = null
|
|
||||||
|
|
||||||
/**
|
BrowseSourceScreen(
|
||||||
* Subscription for the number of manga per row.
|
presenter = presenter,
|
||||||
*/
|
navigateUp = { router.popCurrentController() },
|
||||||
private var numColumnsJob: Job? = null
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
val onDismissRequest = { presenter.dialog = null }
|
||||||
* Endless loading item.
|
when (val dialog = presenter.dialog) {
|
||||||
*/
|
is Dialog.AddDuplicateManga -> {
|
||||||
private var progressItem: ProgressItem? = null
|
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 {
|
LaunchedEffect(presenter.filters) {
|
||||||
setHasOptionsMenu(true)
|
initFilterSheet()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
|
||||||
return presenter.source.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): BrowseSourcePresenter {
|
override fun createPresenter(): BrowseSourcePresenter {
|
||||||
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
|
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() {
|
open fun initFilterSheet() {
|
||||||
if (presenter.sourceFilters.isEmpty()) {
|
if (presenter.filters.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSheet = SourceFilterSheet(
|
filterSheet = SourceFilterSheet(
|
||||||
activity!!,
|
activity!!,
|
||||||
onFilterClicked = {
|
onFilterClicked = {
|
||||||
showProgressBar()
|
presenter.setSourceFilter(presenter.filters)
|
||||||
adapter?.clear()
|
|
||||||
presenter.setSourceFilter(presenter.sourceFilters)
|
|
||||||
},
|
},
|
||||||
onResetClicked = {
|
onResetClicked = {
|
||||||
presenter.appliedFilters = FilterList()
|
presenter.resetFilter()
|
||||||
val newFilters = presenter.source.getFilterList()
|
|
||||||
presenter.sourceFilters = newFilters
|
|
||||||
filterSheet?.setFilters(presenter.filterItems)
|
filterSheet?.setFilters(presenter.filterItems)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
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.
|
* @param newQuery the new query.
|
||||||
*/
|
*/
|
||||||
fun searchWithQuery(newQuery: String) {
|
fun searchWithQuery(newQuery: String) {
|
||||||
// If text didn't change, do nothing
|
presenter.searchQuery = newQuery
|
||||||
if (presenter.query == newQuery) {
|
presenter.search()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgressBar()
|
|
||||||
adapter?.clear()
|
|
||||||
|
|
||||||
presenter.restartPager(newQuery, presenter.sourceFilters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -350,7 +140,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
* @param genreName the name of the genre
|
* @param genreName the name of the genre
|
||||||
*/
|
*/
|
||||||
fun searchWithGenre(genreName: String) {
|
fun searchWithGenre(genreName: String) {
|
||||||
val defaultFilters = presenter.source.getFilterList()
|
val defaultFilters = presenter.source!!.getFilterList()
|
||||||
|
|
||||||
var genreExists = false
|
var genreExists = false
|
||||||
|
|
||||||
@ -380,320 +170,15 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (genreExists) {
|
if (genreExists) {
|
||||||
presenter.sourceFilters = defaultFilters
|
|
||||||
filterSheet?.setFilters(presenter.filterItems)
|
filterSheet?.setFilters(presenter.filterItems)
|
||||||
|
|
||||||
showProgressBar()
|
presenter.searchQuery = ""
|
||||||
|
presenter.setFilter(defaultFilters)
|
||||||
adapter?.clear()
|
|
||||||
presenter.restartPager("", defaultFilters)
|
|
||||||
} else {
|
} else {
|
||||||
searchWithQuery(genreName)
|
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 {
|
protected companion object {
|
||||||
const val SOURCE_ID_KEY = "sourceId"
|
const val SOURCE_ID_KEY = "sourceId"
|
||||||
const val SEARCH_QUERY_KEY = "searchQuery"
|
const val SEARCH_QUERY_KEY = "searchQuery"
|
||||||
|
@ -1,7 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
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.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.GetCategories
|
||||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
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.manga.model.toMangaUpdate
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
import eu.kanade.domain.track.interactor.InsertTrack
|
||||||
import eu.kanade.domain.track.model.toDomainTrack
|
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.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
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.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.ui.browse.source.filter.TriStateSectionItem
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
|
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
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.removeCovers
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -65,6 +84,7 @@ import eu.kanade.domain.manga.model.Manga as DomainManga
|
|||||||
open class BrowseSourcePresenter(
|
open class BrowseSourcePresenter(
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
|
private val state: BrowseSourceStateImpl = BrowseSourceState(searchQuery) as BrowseSourceStateImpl,
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
@ -77,55 +97,76 @@ open class BrowseSourcePresenter(
|
|||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
private val insertTrack: InsertTrack = Injekt.get(),
|
||||||
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
|
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
|
||||||
) : BasePresenter<BrowseSourceController>() {
|
) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
|
||||||
|
|
||||||
/**
|
var displayMode by preferences.sourceDisplayMode().asState()
|
||||||
* Selected source.
|
|
||||||
*/
|
|
||||||
lateinit var source: CatalogueSource
|
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* Modifiable list of filters.
|
fun getColumnsPreferenceForCurrentOrientation(): State<GridCells> {
|
||||||
*/
|
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
var sourceFilters = FilterList()
|
return produceState<GridCells>(initialValue = GridCells.Adaptive(128.dp), isLandscape) {
|
||||||
set(value) {
|
(if (isLandscape) preferences.landscapeColumns() else preferences.portraitColumns())
|
||||||
field = value
|
.asFlow()
|
||||||
filterItems = value.toItems()
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
|
fun getManga(initialManga: DomainManga): State<DomainManga> {
|
||||||
*/
|
return produceState(initialValue = initialManga, initialManga.url, initialManga.source) {
|
||||||
var appliedFilters = FilterList()
|
getManga.subscribe(initialManga.url, initialManga.source)
|
||||||
|
.collectLatest { manga ->
|
||||||
|
if (manga == null) return@collectLatest
|
||||||
|
launchIO {
|
||||||
|
initializeMangas(manga)
|
||||||
|
}
|
||||||
|
value = manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
fun setFilter(filters: FilterList) {
|
||||||
* Pager containing a list of manga results.
|
state.filters = filters
|
||||||
*/
|
}
|
||||||
private lateinit var pager: Pager
|
|
||||||
|
|
||||||
/**
|
fun resetFilter() {
|
||||||
* Subscription for the pager.
|
state.appliedFilters = FilterList()
|
||||||
*/
|
val newFilters = source!!.getFilterList()
|
||||||
private var pagerJob: Job? = null
|
state.filters = newFilters
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
fun search() {
|
||||||
* Subscription for one request from the pager.
|
state.currentQuery = searchQuery ?: ""
|
||||||
*/
|
}
|
||||||
private var nextPageJob: Job? = null
|
|
||||||
|
|
||||||
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
||||||
|
|
||||||
init {
|
|
||||||
query = searchQuery ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
source = sourceManager.get(sourceId) as? CatalogueSource ?: return
|
state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
|
||||||
sourceFilters = source.getFilterList()
|
state.filters = source!!.getFilterList()
|
||||||
|
|
||||||
if (savedState != null) {
|
if (savedState != null) {
|
||||||
query = savedState.getString(::query.name, "")
|
query = savedState.getString(::query.name, "")
|
||||||
@ -137,79 +178,6 @@ open class BrowseSourcePresenter(
|
|||||||
super.onSave(state)
|
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
|
* 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.
|
* if the manga is not yet in the database.
|
||||||
@ -217,16 +185,14 @@ open class BrowseSourcePresenter(
|
|||||||
* @param sManga the manga from the source.
|
* @param sManga the manga from the source.
|
||||||
* @return a manga from the database.
|
* @return a manga from the database.
|
||||||
*/
|
*/
|
||||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
private suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||||
var localManga = runBlocking { getManga.await(sManga.url, sourceId) }
|
var localManga = getManga.await(sManga.url, sourceId)
|
||||||
if (localManga == null) {
|
if (localManga == null) {
|
||||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||||
newManga.copyFrom(sManga)
|
newManga.copyFrom(sManga)
|
||||||
newManga.id = -1
|
newManga.id = -1
|
||||||
val result = runBlocking {
|
val id = insertManga.await(newManga.toDomainManga()!!)
|
||||||
val id = insertManga.await(newManga.toDomainManga()!!)
|
val result = getManga.await(id!!)
|
||||||
getManga.await(id!!)
|
|
||||||
}
|
|
||||||
localManga = result
|
localManga = result
|
||||||
} else if (!localManga.favorite) {
|
} else if (!localManga.favorite) {
|
||||||
// if the manga isn't a favorite, set its display title from source
|
// 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.
|
* @param mangas the list of manga to initialize.
|
||||||
*/
|
*/
|
||||||
fun initializeMangas(mangas: List<DomainManga>) {
|
private suspend fun initializeMangas(manga: DomainManga) {
|
||||||
presenterScope.launchIO {
|
if (manga.thumbnailUrl != null && manga.initialized) return
|
||||||
mangas.asFlow()
|
withContext(NonCancellable) {
|
||||||
.filter { it.thumbnailUrl == null && !it.initialized }
|
val db = manga.toDbManga()
|
||||||
.map { getMangaDetails(it.toDbManga()) }
|
try {
|
||||||
.onEach {
|
val networkManga = source!!.getMangaDetails(db.copy())
|
||||||
withUIContext {
|
db.copyFrom(networkManga)
|
||||||
@Suppress("DEPRECATION")
|
db.initialized = true
|
||||||
view?.onMangaInitialized(it.toDomainManga()!!)
|
updateManga.await(
|
||||||
}
|
db
|
||||||
}
|
.toDomainManga()
|
||||||
.catch { e -> logcat(LogPriority.ERROR, e) }
|
?.toMangaUpdate()!!,
|
||||||
.collect()
|
)
|
||||||
|
} 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.
|
* Adds or removes a manga from the library.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
fun changeMangaFavorite(manga: Manga) {
|
fun changeMangaFavorite(manga: DomainManga) {
|
||||||
manga.favorite = !manga.favorite
|
presenterScope.launch {
|
||||||
manga.date_added = when (manga.favorite) {
|
var new = manga.copy(
|
||||||
true -> Date().time
|
favorite = !manga.favorite,
|
||||||
false -> 0
|
dateAdded = 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()!!,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!new.favorite) {
|
||||||
|
new = new.removeCovers(coverCache)
|
||||||
|
} else {
|
||||||
|
ChapterSettingsHelper.applySettingDefaults(manga)
|
||||||
|
|
||||||
|
autoAddTrack(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateManga.await(new.toMangaUpdate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoAddTrack(manga: Manga) {
|
fun getSourceOrStub(manga: DomainManga): Source {
|
||||||
launchIO {
|
return sourceManager.getOrStub(manga.source)
|
||||||
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()!!)
|
|
||||||
|
|
||||||
val chapters = getChapterByMangaId.await(manga.id!!)
|
fun addFavorite(manga: DomainManga) {
|
||||||
syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service)
|
presenterScope.launch {
|
||||||
}
|
val categories = getCategories()
|
||||||
} catch (e: Exception) {
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" }
|
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.
|
* Set the filter states for the current source.
|
||||||
*
|
*
|
||||||
* @param filters a list of active filters.
|
* @param filters a list of active filters.
|
||||||
*/
|
*/
|
||||||
fun setSourceFilter(filters: FilterList) {
|
fun setSourceFilter(filters: FilterList) {
|
||||||
restartPager(filters = filters)
|
state.appliedFilters = filters
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun createPager(query: String, filters: FilterList): Pager {
|
open fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
|
||||||
return SourcePager(source, query, filters)
|
return SourceBrowsePagingSource(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -395,54 +338,67 @@ open class BrowseSourcePresenter(
|
|||||||
return getDuplicateLibraryManga.await(manga.title, manga.source)
|
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.
|
* Move the given manga to categories.
|
||||||
*
|
*
|
||||||
* @param categories the selected categories.
|
* @param categories the selected categories.
|
||||||
* @param manga the manga to move.
|
* @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 {
|
presenterScope.launchIO {
|
||||||
setMangaCategories.await(
|
setMangaCategories.await(
|
||||||
mangaId = manga.id!!,
|
mangaId = manga.id,
|
||||||
categoryIds = categories.filter { it.id != 0L }.map { it.id },
|
categoryIds = categoryIds.toList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
sealed class Dialog {
|
||||||
* Move the given manga to the category.
|
data class RemoveManga(val manga: DomainManga) : Dialog()
|
||||||
*
|
data class AddDuplicateManga(val manga: DomainManga, val duplicate: DomainManga) : Dialog()
|
||||||
* @param category the selected category.
|
data class ChangeMangaCategory(
|
||||||
* @param manga the manga to move.
|
val manga: DomainManga,
|
||||||
*/
|
val initialSelection: List<CheckboxState.State<DomainCategory>>,
|
||||||
fun moveMangaToCategory(manga: Manga, category: DomainCategory?) {
|
) : Dialog()
|
||||||
moveMangaToCategories(manga, listOfNotNull(category))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun FilterList.toItems(): List<IFlexible<*>> {
|
||||||
* Update manga to use selected categories.
|
return mapNotNull { filter ->
|
||||||
*
|
when (filter) {
|
||||||
* @param manga needed to change
|
is Filter.Header -> HeaderItem(filter)
|
||||||
* @param selectedCategories selected categories
|
is Filter.Separator -> SeparatorItem(filter)
|
||||||
*/
|
is Filter.CheckBox -> CheckboxItem(filter)
|
||||||
fun updateMangaCategories(manga: Manga, selectedCategories: List<DomainCategory>) {
|
is Filter.TriState -> TriStateItem(filter)
|
||||||
if (!manga.favorite) {
|
is Filter.Text -> TextItem(filter)
|
||||||
changeMangaFavorite(manga)
|
is Filter.Select<*> -> SelectItem(filter)
|
||||||
}
|
is Filter.Group<*> -> {
|
||||||
|
val group = GroupItem(filter)
|
||||||
moveMangaToCategories(manga, selectedCategories)
|
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
|
package eu.kanade.tachiyomi.ui.browse.source.latest
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import eu.kanade.domain.source.model.Source
|
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.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
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].
|
* 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))
|
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
@Composable
|
||||||
super.onPrepareOptionsMenu(menu)
|
override fun ComposeContent() {
|
||||||
menu.findItem(R.id.action_search).isVisible = false
|
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() {
|
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
|
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.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.BrowseSourcePresenter
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
|
||||||
|
|
||||||
class LatestUpdatesPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
class LatestUpdatesPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
||||||
|
|
||||||
override fun createPager(query: String, filters: FilterList): Pager {
|
override fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
|
||||||
return LatestUpdatesPager(source)
|
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.domain.manga.model.toDbManga
|
||||||
import eu.kanade.presentation.components.ChangeCategoryDialog
|
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
|
import eu.kanade.presentation.components.DuplicateMangaDialog
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.presentation.manga.MangaScreen
|
import eu.kanade.presentation.manga.MangaScreen
|
||||||
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
|
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
|
||||||
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
|
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
|
||||||
import eu.kanade.presentation.manga.components.DuplicateMangaDialog
|
|
||||||
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
|
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
|
@ -51,6 +51,12 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int {
|
|||||||
return coverCache.deleteFromCache(this, true)
|
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 {
|
fun DomainManga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: PreferencesHelper): Boolean {
|
||||||
if (!favorite) return false
|
if (!favorite) return false
|
||||||
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true" android:color="?attr/colorOnPrimary" />
|
|
||||||
<item android:state_activated="true" android:color="?attr/colorOnPrimary" />
|
|
||||||
<item android:color="?android:attr/textColorPrimary" />
|
|
||||||
</selector>
|
|
@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="2dp"
|
|
||||||
android:background="@drawable/library_item_selector"
|
|
||||||
android:foreground="@drawable/library_item_selector_overlay"
|
|
||||||
android:padding="4dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/thumbnail"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
app:layout_constraintDimensionRatio="w,3:2"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Cover"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
tools:src="@mipmap/ic_launcher" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/badges"
|
|
||||||
layout="@layout/source_grid_item_badges"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/thumbnail"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/thumbnail"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/thumbnail" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
|
||||||
android:textColor="@color/source_comfortable_item_title"
|
|
||||||
android:textSize="12sp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/thumbnail"
|
|
||||||
tools:text="Sample name" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="2dp"
|
|
||||||
android:background="@drawable/library_item_selector"
|
|
||||||
android:foreground="@drawable/library_item_selector_overlay"
|
|
||||||
android:padding="4dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/thumbnail"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
android:foreground="@drawable/card_gradient_shape"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintDimensionRatio="w,2:3"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Cover"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
tools:src="@mipmap/ic_launcher" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/badges"
|
|
||||||
layout="@layout/source_grid_item_badges"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/thumbnail"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/thumbnail"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/thumbnail" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:shadowColor="@color/md_black_1000"
|
|
||||||
android:shadowDx="0"
|
|
||||||
android:shadowDy="0"
|
|
||||||
android:shadowRadius="4"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
|
||||||
android:textColor="@color/md_white_1000"
|
|
||||||
android:textSize="12sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/thumbnail"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
tools:text="Sample name" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/catalogue_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/progress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:indeterminate="true" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.EmptyView
|
|
||||||
android:id="@+id/empty_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
@ -1,116 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:layout_marginEnd="4dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/left_badges"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/rounded_rectangle">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/local_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorTertiary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-condensed"
|
|
||||||
android:text="@string/local_source_badge"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnTertiary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorTertiary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnTertiary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="120"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/unread_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorSecondary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSecondary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="120"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/favorite_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorSecondary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-condensed"
|
|
||||||
android:text="@string/in_library"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSecondary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/right_badges"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/rounded_rectangle">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/language_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorTertiary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-condensed"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnTertiary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="EN"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -1,138 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:background="@drawable/list_item_selector_background"
|
|
||||||
android:paddingHorizontal="8dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/thumbnail"
|
|
||||||
android:layout_width="56dp"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Cover"
|
|
||||||
tools:src="@mipmap/ic_launcher" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/badges"
|
|
||||||
app:layout_constraintHorizontal_bias="0.007"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/thumbnail"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.523"
|
|
||||||
tools:text="Manga title" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/badges"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:background="@drawable/rounded_rectangle"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/local_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorTertiary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-condensed"
|
|
||||||
android:text="@string/local_source_badge"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnTertiary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorTertiary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnTertiary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="122"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/unread_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorSecondary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSecondary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="130"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/favorite_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorSecondary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-condensed"
|
|
||||||
android:text="@string/in_library"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSecondary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/language_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorTertiary"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingStart="3dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="3dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:fontFamily="sans-serif-condensed"
|
|
||||||
tools:text="EN"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnTertiary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="8dp">
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
android:id="@+id/progress_bar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:indeterminate="true"
|
|
||||||
app:indicatorSize="24dp"
|
|
||||||
app:trackThickness="3dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/progress_message"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/no_more_results"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<eu.kanade.tachiyomi.widget.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/source_grid"
|
|
||||||
style="@style/Widget.Tachiyomi.GridView.Source"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:columnWidth="140dp"
|
|
||||||
android:padding="5dp"
|
|
||||||
tools:listitem="@layout/source_compact_grid_item" />
|
|
@ -1,46 +0,0 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_search"
|
|
||||||
android:icon="@drawable/ic_search_24dp"
|
|
||||||
android:title="@string/action_search"
|
|
||||||
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
|
|
||||||
app:iconTint="?attr/colorOnSurface"
|
|
||||||
app:showAsAction="collapseActionView|ifRoom" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:icon="@drawable/ic_view_module_24dp"
|
|
||||||
android:title="@string/action_display_mode"
|
|
||||||
app:iconTint="?attr/colorOnSurface"
|
|
||||||
app:showAsAction="ifRoom">
|
|
||||||
<menu>
|
|
||||||
<group android:checkableBehavior="single">
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_compact_grid"
|
|
||||||
android:title="@string/action_display_grid" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_comfortable_grid"
|
|
||||||
android:title="@string/action_display_comfortable_grid" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_list"
|
|
||||||
android:title="@string/action_display_list" />
|
|
||||||
</group>
|
|
||||||
</menu>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_open_in_web_view"
|
|
||||||
android:icon="@drawable/ic_public_24dp"
|
|
||||||
android:title="@string/action_open_in_web_view"
|
|
||||||
app:iconTint="?attr/colorOnSurface"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_local_source_help"
|
|
||||||
android:icon="@drawable/ic_help_24dp"
|
|
||||||
android:title="@string/local_source_help_guide"
|
|
||||||
app:iconTint="?attr/colorOnSurface"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
</menu>
|
|
@ -870,4 +870,5 @@
|
|||||||
<!-- App widget -->
|
<!-- App widget -->
|
||||||
<string name="appwidget_updates_description">See your recently updated manga</string>
|
<string name="appwidget_updates_description">See your recently updated manga</string>
|
||||||
<string name="appwidget_unavailable_locked">Widget not available when app lock is enabled</string>
|
<string name="appwidget_unavailable_locked">Widget not available when app lock is enabled</string>
|
||||||
|
<string name="remove_manga">You are about to remove this manga from your library</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user