Use Compose on BrowseSourceScreens (#7901)

(cherry picked from commit d4b764fa317ddcb04b8fefb982cd6b9a6dfc1598)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt
#	app/src/main/res/layout/source_comfortable_grid_item.xml
#	app/src/main/res/layout/source_compact_grid_item.xml
#	app/src/main/res/menu/source_browse.xml
This commit is contained in:
Andreas 2022-08-31 20:41:35 +02:00 committed by Jobobby04
parent f4b75fc08a
commit 16ea8aa3b7
71 changed files with 2820 additions and 2536 deletions

View File

@ -281,6 +281,7 @@ dependencies {
// RatingBar (SY) // RatingBar (SY)
implementation(sylibs.ratingbar) implementation(sylibs.ratingbar)
implementation(sylibs.composeRatingbar)
} }
tasks { tasks {

View File

@ -29,6 +29,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) }
} }

View File

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

View File

@ -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>

View File

@ -0,0 +1,82 @@
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
import exh.source.isEhBasedSource
@Composable
fun BrowseLatestScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
) {
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.takeUnless { presenter.source!!.isEhBasedSource() && presenter.ehentaiBrowseDisplayMode },
onDisplayModeChange = { presenter.displayMode = it },
onHelpClick = onHelpClick,
onWebViewClick = onWebViewClick,
// SY -->
onSettingsClick = onSettingsClick,
// SY <--
)
},
) { paddingValues ->
BrowseSourceContent(
source = presenter.source,
mangaList = presenter.getMangaList().collectAsLazyPagingItems(),
getMangaState = { presenter.getManga(it) },
// SY -->
getMetadataState = { manga, metadata ->
presenter.getRaisedSearchMetadata(manga, metadata)
},
// SY <--
columns = columns,
// SY -->
ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode,
// SY <--
displayMode = presenter.displayMode,
snackbarHostState = remember { SnackbarHostState() },
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}

View File

@ -0,0 +1,86 @@
package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHost
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.compose.ui.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
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.library.setting.LibraryDisplayMode
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
@Composable
fun BrowseMangadexFollowsScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onDisplayModeChange: (LibraryDisplayMode) -> 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 = {
BrowseSourceSimpleToolbar(
title = stringResource(R.string.mangadex_follows),
displayMode = presenter.displayMode,
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
source = presenter.source,
mangaList = mangaList,
getMangaState = { presenter.getManga(it) },
// SY -->
getMetadataState = { manga, metadata ->
presenter.getRaisedSearchMetadata(manga, metadata)
},
// SY <--
columns = columns,
// SY -->
ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode,
// SY <--
displayMode = presenter.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}

View File

@ -0,0 +1,73 @@
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.BrowseSourceSimpleToolbar
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 BrowseRecommendationsScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
title: String,
onMangaClick: (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 = {
BrowseSourceSimpleToolbar(
navigateUp = navigateUp,
title = title,
displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it },
)
},
) { paddingValues ->
BrowseSourceContent(
source = presenter.source,
mangaList = presenter.getMangaList().collectAsLazyPagingItems(),
getMangaState = { presenter.getManga(it) },
// SY -->
getMetadataState = { manga, metadata ->
presenter.getRaisedSearchMetadata(manga, metadata)
},
// SY <--
columns = columns,
// SY -->
ehentaiBrowseDisplayMode = false,
// SY <--
displayMode = presenter.displayMode,
snackbarHostState = remember { SnackbarHostState() },
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaClick,
)
}
}

View File

@ -0,0 +1,264 @@
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.BrowseSourceEHentaiList
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
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.isEhBasedSource
@Composable
fun BrowseSourceScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
onFabClick: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
) {
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.takeUnless { presenter.source!!.isEhBasedSource() && presenter.ehentaiBrowseDisplayMode },
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { presenter.search() },
// SY -->
onSettingsClick = onSettingsClick,
// SY <--
)
},
floatingActionButton = {
// SY -->
// if (presenter.filters.isNotEmpty()) {
ExtendedFloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
text = {
Text(
text = if (presenter.filters.isNotEmpty()) {
stringResource(id = R.string.action_filter)
} else {
stringResource(R.string.saved_searches)
},
)
},
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
)
// }
// SY <--
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
source = presenter.source,
mangaList = mangaList,
getMangaState = { presenter.getManga(it) },
// SY -->
getMetadataState = { manga, metadata ->
presenter.getRaisedSearchMetadata(manga, metadata)
},
// SY <--
columns = columns,
// SY -->
ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode,
// SY <--
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</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
columns: GridCells,
ehentaiBrowseDisplayMode: Boolean,
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
}
// SY -->
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
BrowseSourceEHentaiList(
mangaList = mangaList,
getMangaState = getMangaState,
getMetadataState = getMetadataState,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
return
}
// SY <--
when (displayMode) {
LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid(
mangaList = mangaList,
getMangaState = getMangaState,
// SY -->
getMetadataState = getMetadataState,
// SY <--
columns = columns,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
LibraryDisplayMode.List -> {
BrowseSourceList(
mangaList = mangaList,
getMangaState = getMangaState,
// SY -->
getMetadataState = getMetadataState,
// SY <--
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
else -> {
BrowseSourceCompactGrid(
mangaList = mangaList,
getMangaState = getMangaState,
// SY -->
getMetadataState = getMetadataState,
// SY <--
columns = columns,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}
}

View File

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

View File

@ -0,0 +1,28 @@
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,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
) {
BrowseSourceScreen(
presenter = presenter,
navigateUp = navigateUp,
onDisplayModeChange = { presenter.displayMode = (it) },
onFabClick = onFabClick,
onMangaClick = onClickManga,
onMangaLongClick = onClickManga,
// SY -->
onSettingsClick = onSettingsClick,
// SY <--
)
}

View File

@ -0,0 +1,119 @@
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.Settings
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.ConfigurableSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
import exh.source.anyIs
@Composable
fun BrowseLatestToolbar(
navigateUp: () -> Unit,
source: CatalogueSource,
displayMode: LibraryDisplayMode?,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
onHelpClick: () -> Unit,
onWebViewClick: () -> Unit,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
) {
AppBar(
navigateUp = navigateUp,
title = source.name,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
// SY -->
actions = listOfNotNull(
AppBar.Action(
title = "display_mode",
icon = Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
).takeIf { displayMode != null },
// SY <--
if (source is LocalSource) {
AppBar.Action(
title = "help",
icon = Icons.Outlined.Help,
onClick = onHelpClick,
)
} else {
AppBar.Action(
title = stringResource(R.string.action_web_view),
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
)
},
// SY -->
AppBar.Action(
title = stringResource(R.string.action_settings),
icon = Icons.Outlined.Settings,
onClick = onSettingsClick,
).takeIf { source.anyIs<ConfigurableSource>() },
// SY <--
),
)
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 = "",
)
}
},
)
}
},
)
}

View File

@ -0,0 +1,151 @@
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.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.platform.LocalContext
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
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
@Composable
fun BrowseSourceComfortableGrid(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
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.first)
// SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second)
// SY <--
BrowseSourceComfortableGridItem(
manga = manga,
// SY -->
metadata = metadata,
// SY <--
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,
// SY -->
metadata: RaisedSearchMetadata?,
// SY <--
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))
}
},
// SY -->
badgesEnd = {
if (metadata is MangaDexSearchMetadata) {
metadata.followStatus?.let { followStatus ->
val text = LocalContext.current
.resources
.let {
remember {
it.getStringArray(R.array.md_follows_options)
.getOrNull(followStatus)
}
}
?: return@let
Badge(
text = text,
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
metadata.relation?.let {
Badge(
text = stringResource(it.resId),
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
// SY <--
)
MangaGridComfortableText(
text = manga.title,
)
}
}

View File

@ -0,0 +1,174 @@
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.runtime.remember
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.platform.LocalContext
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
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
@Composable
fun BrowseSourceCompactGrid(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
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.first)
// SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second)
// SY <--
BrowseSourceCompactGridItem(
manga = manga,
// SY -->
metadata = metadata,
// SY <--
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,
// SY -->
metadata: RaisedSearchMetadata?,
// SY <--
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))
}
},
// SY -->
badgesEnd = {
if (metadata is MangaDexSearchMetadata) {
metadata.followStatus?.let { followStatus ->
val text = LocalContext.current
.resources
.let {
remember {
it.getStringArray(R.array.md_follows_options)
.getOrNull(followStatus)
}
}
?: return@let
Badge(
text = text,
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
metadata.relation?.let {
Badge(
text = stringResource(it.resId),
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
// SY <--
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)
},
)
}

View File

@ -0,0 +1,39 @@
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.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))
},
)
}

View File

@ -0,0 +1,265 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.items
import com.gowtham.ratingbar.RatingBar
import com.gowtham.ratingbar.RatingBarConfig
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaCover
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.metadata.MetadataUtil
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.util.SourceTagsUtil
import exh.util.SourceTagsUtil.GenreColor
import exh.util.floor
import java.util.Date
@Composable
fun BrowseSourceEHentaiList(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
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.first)
// SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second)
// SY <--
BrowseSourceEHentaiListItem(
manga = manga,
// SY -->
metadata = metadata,
// SY <--
onClick = { onMangaClick(manga) },
onLongClick = { onMangaLongClick(manga) },
)
}
item {
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
}
}
@Composable
fun BrowseSourceEHentaiListItem(
manga: Manga,
// SY -->
metadata: RaisedSearchMetadata?,
// SY <--
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
if (metadata !is EHentaiSearchMetadata) return
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
val resources = LocalContext.current.resources
val languageText by produceState("", metadata) {
value = withIOContext {
val locale = SourceTagsUtil.getLocaleSourceUtil(
metadata.tags
.firstOrNull { it.namespace == EHentaiSearchMetadata.EH_LANGUAGE_NAMESPACE }
?.name,
)
val pageCount = metadata.length
if (locale != null && pageCount != null) {
resources.getQuantityString(R.plurals.browse_language_and_pages, pageCount, pageCount, locale.toLanguageTag().uppercase())
} else if (pageCount != null) {
resources.getQuantityString(R.plurals.num_pages, pageCount, pageCount)
} else locale?.toLanguageTag()?.uppercase().orEmpty()
}
}
val datePosted by produceState("", metadata) {
value = withIOContext {
runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } }
.getOrNull()
.orEmpty()
}
}
val genre by produceState<Pair<GenreColor, Int>?>(null, metadata) {
value = withIOContext {
when (metadata.genre) {
"doujinshi" -> GenreColor.DOUJINSHI_COLOR to R.string.doujinshi
"manga" -> GenreColor.MANGA_COLOR to R.string.manga
"artistcg" -> GenreColor.ARTIST_CG_COLOR to R.string.artist_cg
"gamecg" -> GenreColor.GAME_CG_COLOR to R.string.game_cg
"western" -> GenreColor.WESTERN_COLOR to R.string.western
"non-h" -> GenreColor.NON_H_COLOR to R.string.non_h
"imageset" -> GenreColor.IMAGE_SET_COLOR to R.string.image_set
"cosplay" -> GenreColor.COSPLAY_COLOR to R.string.cosplay
"asianporn" -> GenreColor.ASIAN_PORN_COLOR to R.string.asian_porn
"misc" -> GenreColor.MISC_COLOR to R.string.misc
else -> null
}
}
}
val rating by produceState(0f, metadata) {
value = withIOContext {
val rating = metadata.averageRating?.toFloat()
rating?.div(0.5F)?.floor()?.let { 0.5F.times(it) } ?: 0f
}
}
Row(
modifier = Modifier
.height(148.dp)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box {
MangaCover.Book(
modifier = Modifier
.fillMaxHeight()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = manga.thumbnailUrl,
)
if (manga.favorite) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
Badge(stringResource(R.string.in_library))
}
}
}
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween) {
Column(Modifier.fillMaxWidth()) {
Text(
text = manga.title,
maxLines = 2,
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
style = MaterialTheme.typography.titleSmall,
overflow = TextOverflow.Ellipsis,
)
metadata.uploader?.let {
Text(
text = it,
maxLines = 1,
modifier = Modifier.padding(start = 8.dp),
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp,
)
}
}
Row(
Modifier
.fillMaxWidth()
.padding(bottom = 8.dp, start = 8.dp, end = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.Start,
) {
RatingBar(
value = rating,
onValueChange = {},
onRatingChanged = {},
config = RatingBarConfig().apply {
isIndicator(true)
numStars(5)
size(18.dp)
activeColor(Color(0xFF005ED7))
inactiveColor(Color(0xE1E2ECFF))
},
)
val color = genre?.first?.color
val res = genre?.second
Card(
colors = if (color != null) {
CardDefaults.cardColors(Color(color))
} else CardDefaults.cardColors(),
) {
Text(
text = if (res != null) {
stringResource(res)
} else {
metadata.genre.orEmpty()
},
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
maxLines = 1,
style = MaterialTheme.typography.bodyMedium,
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.End,
) {
Text(
languageText,
maxLines = 1,
fontSize = 14.sp,
)
Text(
datePosted,
maxLines = 1,
fontSize = 14.sp,
)
}
}
}
}
}

View File

@ -0,0 +1,134 @@
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.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.platform.LocalContext
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.verticalPadding
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
@Composable
fun BrowseSourceList(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
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.first)
// SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second)
// SY <--
BrowseSourceListItem(
manga = manga,
// SY -->
metadata = metadata,
// SY <--
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,
// SY -->
metadata: RaisedSearchMetadata?,
// SY <--
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))
}
if (metadata is MangaDexSearchMetadata) {
metadata.followStatus?.let { followStatus ->
val text = LocalContext.current
.resources
.let {
remember {
it.getStringArray(R.array.md_follows_options)
.getOrNull(followStatus)
}
}
?: return@let
Badge(
text = text,
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
metadata.relation?.let {
Badge(
text = stringResource(it.resId),
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
content = {
MangaListItemContent(text = manga.title)
},
)
}

View File

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

View File

@ -0,0 +1,86 @@
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.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.ui.library.setting.LibraryDisplayMode
@Composable
fun BrowseSourceSimpleToolbar(
navigateUp: () -> Unit,
title: String,
displayMode: LibraryDisplayMode?,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
) {
AppBar(
navigateUp = navigateUp,
title = title,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
// SY -->
actions = listOfNotNull(
AppBar.Action(
title = "display_mode",
icon = Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
),
)
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 = "",
)
}
},
)
}
},
)
}

View File

@ -0,0 +1,227 @@
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.material.icons.outlined.Settings
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.ConfigurableSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
import exh.source.anyIs
import kotlinx.coroutines.delay
@Composable
fun BrowseSourceToolbar(
state: BrowseSourceState,
source: CatalogueSource,
displayMode: LibraryDisplayMode?,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
onSearch: () -> Unit,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
) {
if (state.searchQuery == null) {
BrowseSourceRegularToolbar(
source = source,
displayMode = displayMode,
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
onSearchClick = { state.searchQuery = "" },
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
// SY -->
onSettingsClick = onSettingsClick,
// SY <--
)
} 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,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
) {
AppBar(
navigateUp = navigateUp,
title = source.name,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
actions = listOfNotNull(
AppBar.Action(
title = "search",
icon = Icons.Outlined.Search,
onClick = onSearchClick,
),
// SY -->
AppBar.Action(
title = "display_mode",
icon = Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
).takeIf { displayMode != null },
// SY <--
if (source is LocalSource) {
AppBar.Action(
title = "help",
icon = Icons.Outlined.Help,
onClick = onHelpClick,
)
} else {
AppBar.Action(
title = stringResource(R.string.action_web_view),
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
)
},
// SY -->
AppBar.Action(
title = stringResource(R.string.action_settings),
icon = Icons.Outlined.Settings,
onClick = onSettingsClick,
).takeIf { source.anyIs<ConfigurableSource>() },
// SY <--
),
)
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()
}
}

View File

@ -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

View File

@ -91,13 +91,22 @@ fun LibraryComfortableGridItem(
}, },
// SY <-- // SY <--
) )
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,
)
}

View File

@ -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
@ -113,8 +114,16 @@ fun LibraryCompactGridItem(
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
) )
MangaGridCompactText(manga.title)
}
}
@Composable
fun BoxScope.MangaGridCompactText(
text: String,
) {
Text( Text(
text = manga.title, text = text,
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.align(Alignment.BottomStart), .align(Alignment.BottomStart),
@ -128,5 +137,4 @@ fun LibraryCompactGridItem(
), ),
), ),
) )
}
} }

View File

@ -3,6 +3,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.Column import androidx.compose.foundation.layout.Column
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
@ -17,6 +18,64 @@ 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,
// SY -->
showPlayButton: Boolean = false,
playButtonPosition: PlayButtonPosition = PlayButtonPosition.Bottom,
onOpenReader: () -> Unit = {},
// SY <--
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,
)
}
// SY -->
Column(
Modifier
.align(Alignment.TopEnd),
horizontalAlignment = Alignment.End,
) {
// SY <--
if (badgesEnd != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp),
content = badgesEnd,
)
}
// SY -->
if (showPlayButton && playButtonPosition == PlayButtonPosition.Top) {
StartReadingButton(onOpenReader = onOpenReader)
}
}
if (showPlayButton && playButtonPosition == PlayButtonPosition.Bottom) {
StartReadingButton(
Modifier.align(playButtonPosition.alignment),
onOpenReader = onOpenReader,
)
}
// SY <--
}
}
@Composable @Composable
fun LibraryGridCover( fun LibraryGridCover(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -32,22 +91,15 @@ fun LibraryGridCover(
// SY <-- // SY <--
content: @Composable BoxScope.() -> Unit = {}, content: @Composable BoxScope.() -> Unit = {},
) { ) {
Box( MangaGridCover(
modifier = modifier modifier = modifier,
.fillMaxWidth() cover = {
.aspectRatio(MangaCover.Book.ratio),
) {
MangaCover.Book( MangaCover.Book(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
data = mangaCover, data = mangaCover,
) )
content() },
if (downloadCount > 0 || unreadCount > 0) { badgesStart = {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
if (downloadCount > 0) { if (downloadCount > 0) {
Badge( Badge(
text = "$downloadCount", text = "$downloadCount",
@ -58,19 +110,8 @@ fun LibraryGridCover(
if (unreadCount > 0) { if (unreadCount > 0) {
Badge(text = "$unreadCount") Badge(text = "$unreadCount")
} }
} },
} badgesEnd = {
if (isLocal || language.isNotEmpty()) {
// SY -->
Column(
Modifier.align(Alignment.TopEnd),
horizontalAlignment = Alignment.End,
) {
// SY <--
BadgeGroup(
modifier = Modifier
.padding(4.dp),
) {
if (isLocal) { if (isLocal) {
Badge( Badge(
text = stringResource(R.string.local_source_badge), text = stringResource(R.string.local_source_badge),
@ -84,26 +125,14 @@ fun LibraryGridCover(
textColor = MaterialTheme.colorScheme.onTertiary, textColor = MaterialTheme.colorScheme.onTertiary,
) )
} }
} },
// SY --> // SY -->
if (showPlayButton && playButtonPosition == PlayButtonPosition.Top) { showPlayButton = showPlayButton,
StartReadingButton(onOpenReader = onOpenReader) playButtonPosition = playButtonPosition,
}
}
if (showPlayButton && playButtonPosition == PlayButtonPosition.Bottom) {
StartReadingButton(
Modifier.align(playButtonPosition.alignment),
onOpenReader = onOpenReader, onOpenReader = onOpenReader,
)
}
} else if (showPlayButton) {
StartReadingButton(
modifier = Modifier.align(playButtonPosition.alignment),
onOpenReader = onOpenReader,
)
}
// SY <-- // SY <--
} content = content,
)
} }
enum class PlayButtonPosition(val alignment: Alignment) { enum class PlayButtonPosition(val alignment: Alignment) {

View File

@ -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,38 +76,19 @@ fun LibraryListItem(
onLongClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit,
) { ) {
val manga = item.manga val manga = item.manga
Row( MangaListItem(
modifier = Modifier modifier = Modifier.selectedBackground(isSelected),
.selectedBackground(isSelected) title = manga.title,
.height(56.dp) cover = MangaCover(
.combinedClickable(
onClick = { onClick(manga) },
onLongClick = { onLongClick(manga) },
)
.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
eu.kanade.presentation.components.MangaCover.Square(
modifier = Modifier
.padding(vertical = verticalPadding)
.fillMaxHeight(),
data = MangaCover(
manga.id!!, manga.id!!,
manga.source, manga.source,
manga.favorite, manga.favorite,
manga.thumbnail_url, manga.thumbnail_url,
manga.cover_last_modified, manga.cover_last_modified,
), ),
) onClick = { onClick(manga) },
Text( onLongClick = { onLongClick(manga) },
text = manga.title, ) {
modifier = Modifier
.padding(horizontal = horizontalPadding)
.weight(1f),
maxLines = 2,
style = MaterialTheme.typography.bodyMedium,
)
BadgeGroup {
if (item.downloadCount > 0) { if (item.downloadCount > 0) {
Badge( Badge(
text = "${item.downloadCount}", text = "${item.downloadCount}",
@ -131,5 +114,71 @@ fun LibraryListItem(
) )
} }
} }
}
@Composable
fun MangaListItem(
modifier: Modifier = Modifier,
title: String,
cover: MangaCover,
onClick: () -> Unit,
onLongClick: () -> Unit = onClick,
badges: @Composable RowScope.() -> Unit,
) {
MangaListItem(
modifier = modifier,
coverContent = {
Square(
modifier = Modifier
.padding(vertical = verticalPadding)
.fillMaxHeight(),
data = cover,
)
},
badges = badges,
onClick = onClick,
onLongClick = onLongClick,
content = {
MangaListItemContent(title)
},
)
}
@Composable
fun MangaListItem(
modifier: Modifier = Modifier,
coverContent: @Composable RowScope.() -> Unit,
badges: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
onLongClick: () -> Unit,
content: @Composable RowScope.() -> Unit,
) {
Row(
modifier = modifier
.height(56.dp)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
coverContent()
content()
BadgeGroup(content = badges)
} }
} }
@Composable
fun RowScope.MangaListItemContent(
text: String,
) {
Text(
text = text,
modifier = Modifier
.padding(horizontal = horizontalPadding)
.weight(1f),
maxLines = 2,
style = MaterialTheme.typography.bodyMedium,
)
}

View File

@ -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
* *

View File

@ -1,14 +1,17 @@
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 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.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController
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 uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -26,19 +29,30 @@ class SourceSearchController(
this.targetController = targetController this.targetController = targetController
} }
override fun onItemClick(view: View, position: Int): Boolean { @Composable
val manga = (adapter?.getItem(position) as? SourceItem)?.manga ?: return false override fun ComposeContent() {
val migrationListController = targetController as? MigrationListController ?: return false SourceSearchScreen(
presenter = presenter,
navigateUp = { router.popCurrentController() },
onFabClick = { filterSheet?.show() },
// SY -->
onClickManga = { manga ->
val migrationListController = targetController as? MigrationListController ?: return@SourceSearchScreen
val sourceManager = Injekt.get<SourceManager>() val sourceManager = Injekt.get<SourceManager>()
val source = sourceManager.get(manga.source) ?: return false val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
migrationListController.useMangaForMigration(manga, source) migrationListController.useMangaForMigration(manga, source)
router.popCurrentController() router.popCurrentController()
router.popCurrentController() router.popCurrentController()
return true },
} onSettingsClick = {
router.pushController(SourcePreferencesController(presenter.source!!.id))
},
// SY <--
)
override fun onItemLongClick(position: Int) { LaunchedEffect(presenter.filters) {
view?.let { super.onItemClick(it, position) } initFilterSheet()
}
} }
} }

View File

@ -0,0 +1,47 @@
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.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.metadata.metadata.base.RaisedSearchMetadata
abstract class BrowsePagingSource : PagingSource<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?> /*SY <-- */>() {
abstract suspend fun requestNextPage(currentPage: Int): MangasPage
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */> {
val page = params.key ?: 1
val mangasPage = try {
withIOContext {
requestNextPage(page.toInt())
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
// SY -->
val metadata = if (mangasPage is MetadataMangasPage) {
mangasPage.mangasMetadata
} else emptyList()
// SY <--
return LoadResult.Page(
data = mangasPage.mangas
// SY -->
.mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
// SY <--
prevKey = null,
nextKey = if (mangasPage.hasNextPage) page + 1 else null,
)
}
override fun getRefreshKey(state: PagingState<Long, Pair<SManga, RaisedSearchMetadata?>>): Long? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey ?: anchorPage?.nextKey
}
}
}

View File

@ -1,86 +1,35 @@
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 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.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.presentation.browse.BrowseSourceScreen
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.DuplicateMangaDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource
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.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.source.online.LoginSource
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.extension.details.SourcePreferencesController import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
import eu.kanade.tachiyomi.ui.browse.source.SourcesController import eu.kanade.tachiyomi.ui.browse.source.SourcesController
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.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
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.getParcelableCompat
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast 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 eu.kanade.tachiyomi.widget.materialdialogs.setTextInput import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
import exh.log.xLogW
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.source.anyIs import exh.util.nullIfBlank
import exh.source.getMainSource
import exh.source.isEhBasedSource
import exh.widget.preference.MangadexLoginDialog
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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( constructor(
sourceId: Long, sourceId: Long,
@ -145,48 +94,75 @@ open class BrowseSourceController(bundle: Bundle) :
filterList, filterList,
) )
private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
/* SY --> */
protected /* SY <-- */ 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)
}
}
},
// SY -->
onSettingsClick = {
router.pushController(SourcePreferencesController(presenter.source!!.id))
},
// SY <--
)
/** val onDismissRequest = { presenter.dialog = null }
* Endless loading item. when (val dialog = presenter.dialog) {
*/ is Dialog.AddDuplicateManga -> {
private var progressItem: ProgressItem? = null DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
init { onConfirm = { presenter.addFavorite(dialog.manga) },
setHasOptionsMenu(true) 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 -> {}
} }
override fun getTitle(): String? { LaunchedEffect(presenter.filters) {
return presenter.source.name initFilterSheet()
}
} }
override fun createPresenter(): BrowseSourcePresenter { override fun createPresenter(): BrowseSourcePresenter {
@ -194,61 +170,29 @@ open class BrowseSourceController(bundle: Bundle) :
return BrowseSourcePresenter( return BrowseSourcePresenter(
args.getLong(SOURCE_ID_KEY), args.getLong(SOURCE_ID_KEY),
args.getString(SEARCH_QUERY_KEY), args.getString(SEARCH_QUERY_KEY),
filters = args.getString(FILTERS_CONFIG_KEY), filtersJson = args.getString(FILTERS_CONFIG_KEY),
savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L }, savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L },
) )
// SY <-- // SY <--
} }
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
// SY -->
val mainSource = presenter.source.getMainSource<LoginSource>()
if (mainSource != null && mainSource.requiresLogin && !mainSource.isLogged()) {
val dialog = MangadexLoginDialog(mainSource)
dialog.showDialog(router)
}
// SY <--
presenter.restartPager()
}
fun setSavedSearches(savedSearches: List<EXHSavedSearch>) { fun setSavedSearches(savedSearches: List<EXHSavedSearch>) {
filterSheet?.setSavedSearches(savedSearches) filterSheet?.setSavedSearches(savedSearches)
} }
open fun initFilterSheet() { open fun initFilterSheet() {
if (presenter.sourceFilters.isEmpty()) {
// SY -->
actionFab?.text = activity!!.getString(R.string.saved_searches)
// SY <--
}
filterSheet = SourceFilterSheet( filterSheet = SourceFilterSheet(
activity!!, activity!!,
// SY --> // SY -->
this, this,
presenter.source, presenter.source!!,
emptyList(), emptyList(),
// SY <-- // SY <--
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)
}, },
// EXH --> // EXH -->
@ -264,7 +208,7 @@ open class BrowseSourceController(bundle: Bundle) :
} }
.setPositiveButton(R.string.action_save) { _, _ -> .setPositiveButton(R.string.action_save) { _, _ ->
if (searchName.isNotBlank() && searchName !in names) { if (searchName.isNotBlank() && searchName !in names) {
presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters) presenter.saveSearch(searchName.trim(), presenter.searchQuery.orEmpty(), presenter.filters)
} else { } else {
it.toast(R.string.save_search_invalid_name) it.toast(R.string.save_search_invalid_name)
} }
@ -293,14 +237,14 @@ open class BrowseSourceController(bundle: Bundle) :
return@launchUI return@launchUI
} }
presenter.sourceFilters = FilterList(search.filterList) presenter.setFilter(FilterList(search.filterList))
filterSheet?.setFilters(presenter.filterItems) filterSheet?.setFilters(presenter.filterItems)
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.filters == presenter.source!!.getFilterList()
showProgressBar()
adapter?.clear()
filterSheet?.dismiss() filterSheet?.dismiss()
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters) presenter.searchQuery = search.query.nullIfBlank()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.filters)
presenter.search()
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
}, },
@ -322,199 +266,16 @@ open class BrowseSourceController(bundle: Bundle) :
filterSheet?.setSavedSearches(presenter.loadSearches()) filterSheet?.setSavedSearches(presenter.loadSearches())
} }
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 /* SY --> */ || (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) /* SY <-- */) {
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
// SY -->
if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) {
menu.findItem(R.id.action_display_mode).isVisible = false
}
// SY <--
}
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
// SY -->
menu.findItem(R.id.action_settings)?.isVisible = presenter.source.anyIs<ConfigurableSource>()
// SY <--
}
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()
// SY -->
R.id.action_settings -> openSourceSettings()
// SY <--
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)
}
// SY -->
private fun openSourceSettings() {
router.pushController(SourcePreferencesController(presenter.source.id))
}
// SY <--
/** /**
* Restarts the request with a new query. * Restarts the request with a new query.
* *
* @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)
} }
/** /**
@ -525,7 +286,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
@ -555,335 +316,16 @@ 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)
} }
} }
/** protected companion object {
* 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.
*/
/* SY --> */
open /* SY <-- */fun onAddPageError(error: Throwable) {
// SY -->
xLogW("> Failed to load next catalogue page!", error)
xLogW(
"> (source.id: %s, source.name: %s)",
presenter.source.id,
presenter.source.name,
)
// SY <--
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,
args.getParcelableCompat(MangaController.SMART_SEARCH_CONFIG_EXTRA),
),
)
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))
}
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"

View File

@ -1,12 +1,31 @@
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
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.InsertManga import eu.kanade.domain.manga.interactor.InsertManga
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
@ -17,6 +36,8 @@ import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.InsertSavedSearch import eu.kanade.domain.source.interactor.InsertSavedSearch
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
@ -25,10 +46,12 @@ 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
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoComplete import eu.kanade.tachiyomi.ui.browse.source.filter.AutoComplete
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoCompleteSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.AutoCompleteSectionItem
@ -47,23 +70,25 @@ 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.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
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 exh.metadata.metadata.base.RaisedSearchMetadata
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import exh.source.getMainSource
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import exh.util.nullIfBlank import exh.util.nullIfBlank
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.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -80,9 +105,10 @@ open class BrowseSourcePresenter(
private val sourceId: Long, private val sourceId: Long,
searchQuery: String? = null, searchQuery: String? = null,
// SY --> // SY -->
private val filters: String? = null, private val filtersJson: String? = null,
private val savedSearch: Long? = null, private val savedSearch: Long? = null,
// SY <-- // SY <--
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(),
@ -95,48 +121,91 @@ 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(),
// SY --> // SY -->
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(), private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(),
private val insertSavedSearch: InsertSavedSearch = Injekt.get(), private val insertSavedSearch: InsertSavedSearch = Injekt.get(),
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(), private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
// SY <-- // SY <--
) : BasePresenter<BrowseSourceController>() { ) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
/** var displayMode by preferences.sourceDisplayMode().asState()
* Selected source.
*/
lateinit var source: CatalogueSource
/** val ehentaiBrowseDisplayMode by preferences.enhancedEHentaiView().asState()
* Modifiable list of filters.
*/ @Composable
var sourceFilters = FilterList() fun getColumnsPreferenceForCurrentOrientation(): State<GridCells> {
set(value) { val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
field = value return produceState<GridCells>(initialValue = GridCells.Adaptive(128.dp), isLandscape) {
filterItems = value.toItems() (if (isLandscape) preferences.landscapeColumns() else preferences.portraitColumns())
.asFlow()
.collectLatest { columns ->
value = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
}
}
} }
var filterItems: List<IFlexible<*>> = emptyList() @Composable
fun getMangaList(): Flow<PagingData</* SY --> */Pair<DomainManga, RaisedSearchMetadata?>/* SY <-- */>> {
return remember(currentQuery, appliedFilters) {
Pager(
PagingConfig(pageSize = 25),
) {
createPager(currentQuery, appliedFilters)
}.flow
.map {
it.map {
// SY -->
withIOContext {
networkToLocalManga(it.first, sourceId).toDomainManga()!!
} to it.second
// SY <--
}
}
.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
}
}
}
/** @Composable
* Pager containing a list of manga results. open fun getRaisedSearchMetadata(manga: DomainManga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
*/ return produceState(initialValue = initialMetadata, manga.id) {
private lateinit var pager: Pager val source = source?.getMainSource<MetadataSource<*, *>>() ?: return@produceState
getFlatMetadataById.subscribe(manga.id)
.collectLatest { metadata ->
if (metadata == null) return@collectLatest
value = metadata.raise(source.metaClass)
}
}
}
/** fun setFilter(filters: FilterList) {
* Subscription for the pager. state.filters = filters
*/ }
private var pagerJob: Job? = null
/** fun resetFilter() {
* Subscription for one request from the pager. state.appliedFilters = FilterList()
*/ val newFilters = source!!.getFilterList()
private var nextPageJob: Job? = null state.filters = newFilters
}
fun search() {
state.currentQuery = searchQuery ?: ""
}
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } } private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
@ -144,36 +213,32 @@ open class BrowseSourcePresenter(
private val filterSerializer = FilterSerializer() private val filterSerializer = FilterSerializer()
// SY <-- // SY <--
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()
// SY --> // SY -->
val savedSearchFilters = savedSearch val savedSearchFilters = savedSearch
val jsonFilters = filters val jsonFilters = filtersJson
if (savedSearchFilters != null) { if (savedSearchFilters != null) {
val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { sourceFilters } } val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { filters } }
if (savedSearch != null) { if (savedSearch != null) {
query = savedSearch.query query = savedSearch.query
if (savedSearch.filterList != null) { if (savedSearch.filterList != null) {
appliedFilters = savedSearch.filterList setFilter(savedSearch.filterList)
} }
} }
} else if (jsonFilters != null) { } else if (jsonFilters != null) {
runCatching { runCatching {
val filters = Json.decodeFromString<JsonArray>(jsonFilters) val filters = Json.decodeFromString<JsonArray>(jsonFilters)
filterSerializer.deserialize(sourceFilters, filters) filterSerializer.deserialize(this.filters, filters)
appliedFilters = sourceFilters setSourceFilter(this.filters)
} }
} }
getExhSavedSearch.subscribe(source.id, source::getFilterList) getExhSavedSearch.subscribe(source!!.id, source!!::getFilterList)
.onEach { .onEach {
withUIContext { withUIContext {
view?.setSavedSearches(it) view?.setSavedSearches(it)
@ -192,88 +257,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()
// SY -->
.map { (first, second, third) ->
Triple(
first,
second.map {
networkToLocalManga(
it,
sourceId,
).toDomainManga()!!
},
third,
)
}
// SY <--
.onEach { initializeMangas(it.second) }
// SY -->
.map { (first, second, third) ->
first to second.mapIndexed { index, manga ->
SourceItem(
manga,
sourceDisplayMode,
third?.getOrNull(index),
)
}
}
// SY <--
.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.
@ -281,16 +264,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()!!)
getManga.await(id!!) val result = 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
@ -301,46 +282,27 @@ 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()) }
.onEach {
withUIContext {
@Suppress("DEPRECATION")
view?.onMangaInitialized(it.toDomainManga()!!)
}
}
.catch { e -> logcat(LogPriority.ERROR, e) }
.collect()
}
}
/**
* Returns the initialized manga.
*
* @param manga the manga to initialize.
* @return the initialized manga
*/
private suspend fun getMangaDetails(manga: Manga): Manga {
try { try {
val networkManga = source.getMangaDetails(manga.copy()) val networkManga = source!!.getMangaDetails(db.copy())
manga.copyFrom(networkManga) db.copyFrom(networkManga)
manga.initialized = true db.initialized = true
updateManga.await( updateManga.await(
manga db
.toDomainManga() .toDomainManga()
?.toMangaUpdate()!!, ?.toMangaUpdate()!!,
) )
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
} }
return manga }
} }
/** /**
@ -348,43 +310,76 @@ open class BrowseSourcePresenter(
* *
* @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(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> Date().time true -> Date().time
false -> 0 false -> 0
} },
)
if (!manga.favorite) { if (!new.favorite) {
manga.removeCovers(coverCache) new = new.removeCovers(coverCache)
} else { } else {
ChapterSettingsHelper.applySettingDefaults(manga.toDomainManga()!!) ChapterSettingsHelper.applySettingDefaults(manga)
autoAddTrack(manga) autoAddTrack(manga)
} }
runBlocking { updateManga.await(new.toMangaUpdate())
updateManga.await(
manga
.toDomainManga()
?.toMangaUpdate()!!,
)
} }
} }
private fun autoAddTrack(manga: Manga) { fun getSourceOrStub(manga: DomainManga): Source {
launchIO { return sourceManager.getOrStub(manga.source)
}
fun addFavorite(manga: DomainManga) {
presenterScope.launch {
val categories = getCategories()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
moveMangaToCategories(manga, defaultCategory)
changeMangaFavorite(manga)
// activity.toast(activity.getString(R.string.manga_added_library))
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
moveMangaToCategories(manga)
changeMangaFavorite(manga)
// activity.toast(activity.getString(R.string.manga_added_library))
}
// Choose a category
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
state.dialog = Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds })
}
}
}
}
private suspend fun autoAddTrack(manga: DomainManga) {
loggedServices loggedServices
.filterIsInstance<EnhancedTrackService>() .filterIsInstance<EnhancedTrackService>()
.filter { it.accept(source) } .filter { it.accept(source!!) }
.forEach { service -> .forEach { service ->
try { try {
service.match(manga)?.let { track -> service.match(manga.toDbManga())?.let { track ->
track.manga_id = manga.id!! track.manga_id = manga.id
(service as TrackService).bind(track) (service as TrackService).bind(track)
insertTrack.await(track.toDomainTrack()!!) insertTrack.await(track.toDomainTrack()!!)
val chapters = getChapterByMangaId.await(manga.id!!) val chapters = getChapterByMangaId.await(manga.id)
syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service) syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -392,7 +387,6 @@ open class BrowseSourcePresenter(
} }
} }
} }
}
/** /**
* Set the filter states for the current source. * Set the filter states for the current source.
@ -400,23 +394,93 @@ open class BrowseSourcePresenter(
* @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, Pair<SManga, RaisedSearchMetadata?>> {
// SY --> // SY -->
return if (source.isEhBasedSource()) { return if (source!!.isEhBasedSource()) {
EHentaiPager(source, query, filters) EHentaiBrowsePagingSource(source!!, query, filters)
} else { } else {
SourcePager(source, query, filters) SourceBrowsePagingSource(source!!, query, filters)
} }
// SY <-- // SY <--
} }
// SY --> /**
companion object { * Get user categories.
// SY <-- *
fun FilterList.toItems(): List<IFlexible<*>> { * @return List of categories, not including the default category
*/
suspend fun getCategories(): List<DomainCategory> {
return getCategories.subscribe()
.firstOrNull()
?.filterNot { it.isSystemCategory }
?: emptyList()
}
suspend fun getDuplicateLibraryManga(manga: DomainManga): DomainManga? {
return getDuplicateLibraryManga.await(manga.title, manga.source)
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
fun moveMangaToCategories(manga: DomainManga, vararg categories: DomainCategory) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
}
fun moveMangaToCategories(manga: DomainManga, categoryIds: List<Long>) {
presenterScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
)
}
}
sealed class Dialog {
data class RemoveManga(val manga: DomainManga) : Dialog()
data class AddDuplicateManga(val manga: DomainManga, val duplicate: DomainManga) : Dialog()
data class ChangeMangaCategory(
val manga: DomainManga,
val initialSelection: List<CheckboxState.State<DomainCategory>>,
) : Dialog()
}
// EXH -->
fun saveSearch(name: String, query: String, filterList: FilterList) {
launchIO {
insertSavedSearch.await(
SavedSearch(
id = -1,
source = source!!.id,
name = name.trim(),
query = query.nullIfBlank(),
filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(),
),
)
}
}
fun deleteSearch(savedSearchId: Long) {
launchIO {
deleteSavedSearchById.await(savedSearchId)
}
}
suspend fun loadSearch(searchId: Long) =
getExhSavedSearch.awaitOne(searchId, source!!::getFilterList)
suspend fun loadSearches() =
getExhSavedSearch.await(source!!.id, source!!::getFilterList)
// EXH <--
}
fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull { filter -> return mapNotNull { filter ->
when (filter) { when (filter) {
is Filter.Header -> HeaderItem(filter) is Filter.Header -> HeaderItem(filter)
@ -456,103 +520,4 @@ open class BrowseSourcePresenter(
} }
} }
} }
}
// SY -->
}
// SY <--
/**
* Get user categories.
*
* @return List of categories, not including the default category
*/
suspend fun getCategories(): List<DomainCategory> {
return getCategories.subscribe()
.firstOrNull()
?.filterNot { it.isSystemCategory }
?: emptyList()
}
suspend fun getDuplicateLibraryManga(manga: DomainManga): DomainManga? {
return getDuplicateLibraryManga.await(manga.title, manga.source)
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: DomainManga): Array<Long?> {
return runBlocking { getCategories.await(manga.id) }
.map { it.id }
.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<DomainCategory>) {
presenterScope.launchIO {
setMangaCategories.await(
mangaId = manga.id!!,
categoryIds = categories.filter { it.id != 0L }.map { it.id },
)
}
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: DomainCategory?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<DomainCategory>) {
if (!manga.favorite) {
changeMangaFavorite(manga)
}
moveMangaToCategories(manga, selectedCategories)
}
// EXH -->
fun saveSearch(name: String, query: String, filterList: FilterList) {
launchIO {
insertSavedSearch.await(
SavedSearch(
id = -1,
source = source.id,
name = name.trim(),
query = query.nullIfBlank(),
filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(),
),
)
}
}
fun deleteSearch(savedSearchId: Long) {
launchIO {
deleteSavedSearchById.await(savedSearchId)
}
}
suspend fun loadSearch(searchId: Long) =
getExhSavedSearch.awaitOne(searchId, source::getFilterList)
suspend fun loadSearches() =
getExhSavedSearch.await(source.id, source::getFilterList)
// EXH <--
} }

View File

@ -0,0 +1,49 @@
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.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.util.lang.awaitSingle
class EHentaiBrowsePagingSource(val source: CatalogueSource, val query: String, val filters: FilterList) : BrowsePagingSource() {
private var lastMangaLink: String? = null
override suspend fun requestNextPage(currentPage: Int): MangasPage {
val lastMangaLink = lastMangaLink
val observable = if (query.isBlank() && filters.isEmpty()) {
source.fetchPopularManga(currentPage)
} else {
source.fetchSearchManga(currentPage, query, filters)
}
val mangasPage = observable.awaitSingle()
mangasPage.mangas.lastOrNull()?.let {
this.lastMangaLink = it.url
}
return if (lastMangaLink != null) {
val index = mangasPage.mangas.indexOfFirst { it.url == lastMangaLink }
if (index != -1) {
val lastIndex = mangasPage.mangas.size
val startIndex = (index + 1).coerceAtMost(mangasPage.mangas.lastIndex)
if (mangasPage is MetadataMangasPage) {
mangasPage.copy(
mangas = mangasPage.mangas.subList(startIndex, lastIndex),
mangasMetadata = mangasPage.mangasMetadata.subList(startIndex, lastIndex),
)
} else {
mangasPage.copy(
mangas = mangasPage.mangas.subList(startIndex, lastIndex),
)
}
} else {
mangasPage
}
} else {
mangasPage
}
}
}

View File

@ -1,51 +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.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.util.lang.awaitSingle
class EHentaiPager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() {
private var lastMangaLink: String? = null
override suspend fun requestNextPage() {
val page = currentPage
val lastMangaLink = lastMangaLink
val observable = if (query.isBlank() && filters.isEmpty()) {
source.fetchPopularManga(page)
} else {
source.fetchSearchManga(page, query, filters)
}
val mangasPage = observable.awaitSingle()
mangasPage.mangas.lastOrNull()?.let {
this.lastMangaLink = it.url
}
if (lastMangaLink != null) {
val index = mangasPage.mangas.indexOfFirst { it.url == lastMangaLink }
if (index != -1) {
val lastIndex = mangasPage.mangas.size
val startIndex = (index + 1).coerceAtMost(mangasPage.mangas.lastIndex)
onPageReceived(
if (mangasPage is MetadataMangasPage) {
mangasPage.copy(
mangas = mangasPage.mangas.subList(startIndex, lastIndex),
mangasMetadata = mangasPage.mangasMetadata.subList(startIndex, lastIndex),
)
} else {
mangasPage.copy(
mangas = mangasPage.mangas.subList(startIndex, lastIndex),
)
},
)
} else {
onPageReceived(mangasPage)
}
} else {
onPageReceived(mangasPage)
}
}
}

View File

@ -1,38 +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.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.metadata.base.RaisedSearchMetadata
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</* SY --> */ Triple /* SY <-- */<Int, List<SManga> /* SY --> */, List<RaisedSearchMetadata>? /* SY <-- */>> = PublishRelay.create()
fun asFlow(): Flow</* SY --> */ Triple /* SY <-- */<Int, List<SManga> /* SY --> */, List<RaisedSearchMetadata>?> /* SY <-- */> {
return results.asObservable().asFlow()
}
abstract suspend fun requestNextPage()
fun onPageReceived(mangasPage: MangasPage) {
val page = currentPage
currentPage++
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
// SY -->
val mangasMetadata = if (mangasPage is MetadataMangasPage) {
mangasPage.mangasMetadata
} else null
// SY <--
results.call( /* SY <-- */ Triple /* SY <-- */ (page, mangasPage.mangas /* SY --> */, mangasMetadata /* SY <-- */))
}
}

View File

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

View File

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

View File

@ -1,71 +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.R
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
import eu.kanade.tachiyomi.util.view.loadAutoPause
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
/**
* 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)
}
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.followStatus?.let {
binding.badges.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.badges.localText.isVisible = true
}
metadata.relation?.let {
binding.badges.localText.setText(it.resId)
binding.badges.localText.isVisible = true
}
}
}
// SY <--
override fun setImage(manga: Manga) {
binding.thumbnail.dispose()
binding.thumbnail.loadAutoPause(manga) {
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
}
}
}

View File

@ -1,71 +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.R
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
import eu.kanade.tachiyomi.util.view.loadAutoPause
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
/**
* 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)
}
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.followStatus?.let {
binding.badges.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.badges.localText.isVisible = true
}
metadata.relation?.let {
binding.badges.localText.setText(it.resId)
binding.badges.localText.isVisible = true
}
}
}
// SY <--
override fun setImage(manga: Manga) {
binding.thumbnail.dispose()
binding.thumbnail.loadAutoPause(manga) {
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
}
}
}

View File

@ -1,108 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View
import androidx.core.view.isVisible
import coil.dispose
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.SourceEnhancedEhentaiListItemBinding
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.loadAutoPause
import exh.metadata.MetadataUtil
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.util.SourceTagsUtil
import java.util.Date
/**
* 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 SourceEnhancedEHentaiListHolder(view: View, adapter: FlexibleAdapter<*>) :
SourceHolder<SourceEnhancedEhentaiListItemBinding>(view, adapter) {
override val binding = SourceEnhancedEhentaiListItemBinding.bind(itemView)
private val favoriteColor = itemView.context.getResourceColor(R.attr.colorOnSurface, 0.38f)
private val unfavoriteColor = itemView.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.leftBadges.clipToOutline = true
binding.badges.rightBadges.clipToOutline = true
// Set favorite badge
binding.badges.favoriteText.isVisible = manga.favorite
setImage(manga)
}
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata !is EHentaiSearchMetadata) return
if (metadata.uploader != null) {
binding.uploader.text = metadata.uploader
}
val pair = when (metadata.genre) {
"doujinshi" -> SourceTagsUtil.GenreColor.DOUJINSHI_COLOR to R.string.doujinshi
"manga" -> SourceTagsUtil.GenreColor.MANGA_COLOR to R.string.manga
"artistcg" -> SourceTagsUtil.GenreColor.ARTIST_CG_COLOR to R.string.artist_cg
"gamecg" -> SourceTagsUtil.GenreColor.GAME_CG_COLOR to R.string.game_cg
"western" -> SourceTagsUtil.GenreColor.WESTERN_COLOR to R.string.western
"non-h" -> SourceTagsUtil.GenreColor.NON_H_COLOR to R.string.non_h
"imageset" -> SourceTagsUtil.GenreColor.IMAGE_SET_COLOR to R.string.image_set
"cosplay" -> SourceTagsUtil.GenreColor.COSPLAY_COLOR to R.string.cosplay
"asianporn" -> SourceTagsUtil.GenreColor.ASIAN_PORN_COLOR to R.string.asian_porn
"misc" -> SourceTagsUtil.GenreColor.MISC_COLOR to R.string.misc
else -> null
}
if (pair != null) {
binding.genre.setBackgroundColor(pair.first.color)
binding.genre.text = itemView.context.getString(pair.second)
} else binding.genre.text = metadata.genre
metadata.datePosted?.let { binding.datePosted.text = MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }
metadata.averageRating?.let { binding.ratingBar.rating = it.toFloat() }
val locale = SourceTagsUtil.getLocaleSourceUtil(
metadata.tags
.firstOrNull { it.namespace == EHentaiSearchMetadata.EH_LANGUAGE_NAMESPACE }
?.name,
)
val pageCount = metadata.length
binding.language.text = if (locale != null && pageCount != null) {
itemView.resources.getQuantityString(R.plurals.browse_language_and_pages, pageCount, pageCount, locale.toLanguageTag().uppercase())
} else if (pageCount != null) {
itemView.resources.getQuantityString(R.plurals.num_pages, pageCount, pageCount)
} else locale?.toLanguageTag()?.uppercase()
}
override fun setImage(manga: Manga) {
binding.thumbnail.dispose()
binding.thumbnail.loadAutoPause(manga) {
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
}
}
}

View File

@ -1,40 +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
import exh.metadata.metadata.base.RaisedSearchMetadata
/**
* 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)
// SY -->
abstract fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata)
// SY <--
}

View File

@ -1,85 +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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.isEhBasedManga
import uy.kohesive.injekt.injectLazy
class SourceItem(val manga: Manga, private val displayMode: Preference<LibraryDisplayMode> /* SY --> */, private val metadata: RaisedSearchMetadata? = null /* SY <-- */) :
AbstractFlexibleItem<SourceHolder<*>>() {
// SY -->
val preferences: PreferencesHelper by injectLazy()
// SY <--
override fun getLayoutRes(): Int {
// SY -->
if (manga.isEhBasedManga() && preferences.enhancedEHentaiView().get()) {
return R.layout.source_enhanced_ehentai_list_item
}
// SY <--
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<*> {
// SY -->
if (manga.isEhBasedManga() && preferences.enhancedEHentaiView().get()) {
return SourceEnhancedEHentaiListHolder(view, adapter)
}
// SY <--
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)
// SY -->
if (metadata != null) {
holder.onSetMetadataValues(manga, metadata)
}
// SY <--
}
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()
}
}

View File

@ -1,77 +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
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
/**
* 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)
}
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.followStatus?.let {
binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.localText.isVisible = true
}
metadata.relation?.let {
binding.localText.setText(it.resId)
binding.localText.isVisible = true
}
}
}
// SY <--
override fun setImage(manga: Manga) {
binding.thumbnail.dispose()
if (!manga.thumbnailUrl.isNullOrEmpty()) {
binding.thumbnail.load(manga) {
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
}
}
}
}

View File

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

View File

@ -27,7 +27,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat

View File

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

View File

@ -1,13 +1,22 @@
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.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
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].
@ -28,9 +37,68 @@ 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)
}
}
},
// SY -->
onSettingsClick = {
router.pushController(SourcePreferencesController(presenter.source!!.id))
},
// SY <--
)
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() {

View File

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

View File

@ -1,12 +1,14 @@
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 import exh.metadata.metadata.base.RaisedSearchMetadata
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, /*SY --> */ Pair<SManga, RaisedSearchMetadata?> /*SY <-- */> {
return LatestUpdatesPager(source) return LatestUpdatesBrowsePagingSource(source!!)
} }
} }

View File

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

View File

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

View File

@ -23,12 +23,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

View File

@ -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

View File

@ -1,12 +1,20 @@
package exh.md.follows package exh.md.follows
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.tachiyomi.R import eu.kanade.presentation.browse.BrowseMangadexFollowsScreen
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.DuplicateMangaDialog
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
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].
@ -19,22 +27,69 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle)
), ),
) )
override fun getTitle(): String? {
return view?.context?.getString(R.string.mangadex_follows)
}
override fun createPresenter(): BrowseSourcePresenter { override fun createPresenter(): BrowseSourcePresenter {
return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY)) return MangaDexFollowsPresenter(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()
menu.findItem(R.id.action_open_in_web_view).isVisible = false
menu.findItem(R.id.action_settings).isVisible = false BrowseMangadexFollowsScreen(
presenter = presenter,
navigateUp = { router.popCurrentController() },
onDisplayModeChange = { presenter.displayMode = (it) },
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,
onConfirm = { presenter.addFavorite(dialog.manga) },
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
)
}
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() {
// No-op: we don't allow filtering in latest // No-op: we don't allow filtering in mangadex follows
} }
} }

View File

@ -1,14 +0,0 @@
package exh.md.follows
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
/**
* LatestUpdatesPager inherited from the general Pager.
*/
class MangaDexFollowsPager(val source: MangaDex) : Pager() {
override suspend fun requestNextPage() {
onPageReceived(source.fetchFollows(currentPage))
}
}

View File

@ -0,0 +1,15 @@
package exh.md.follows
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
/**
* LatestUpdatesPager inherited from the general Pager.
*/
class MangaDexFollowsPagingSource(val source: MangaDex) : BrowsePagingSource() {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchFollows(currentPage)
}
}

View File

@ -1,9 +1,16 @@
package exh.md.follows package exh.md.follows
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.paging.PagingSource
import eu.kanade.domain.manga.model.Manga
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.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
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 import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource import exh.source.getMainSource
/** /**
@ -11,8 +18,12 @@ import exh.source.getMainSource
*/ */
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) { class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager { override fun createPager(query: String, filters: FilterList): PagingSource<Long, Pair<SManga, RaisedSearchMetadata?>> {
val sourceAsMangaDex = source.getMainSource() as MangaDex return MangaDexFollowsPagingSource(source!!.getMainSource() as MangaDex)
return MangaDexFollowsPager(sourceAsMangaDex) }
@Composable
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
return remember { mutableStateOf(initialMetadata) }
} }
} }

View File

@ -1,13 +1,17 @@
package exh.md.similar package exh.md.similar
import android.os.Bundle import android.os.Bundle
import android.view.Menu import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.BrowseRecommendationsScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
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.manga.MangaController
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
@ -22,36 +26,28 @@ class MangaDexSimilarController(bundle: Bundle) : BrowseSourceController(bundle)
), ),
) )
private val mangaTitle = args.getString(MANGA_TITLE) private val mangaTitle = args.getString(MANGA_TITLE, "")
override fun getTitle(): String? {
return view?.context?.getString(R.string.similar, mangaTitle)
}
override fun createPresenter(): BrowseSourcePresenter { override fun createPresenter(): BrowseSourcePresenter {
return MangaDexSimilarPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY)) return MangaDexSimilarPresenter(args.getLong(MANGA_ID), 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 BrowseRecommendationsScreen(
menu.findItem(R.id.action_open_in_web_view).isVisible = false presenter = presenter,
menu.findItem(R.id.action_settings).isVisible = false navigateUp = { router.popCurrentController() },
title = stringResource(R.string.similar, mangaTitle),
onMangaClick = {
router.pushController(MangaController(it.id, true))
},
)
} }
override fun initFilterSheet() { override fun initFilterSheet() {
// No-op: we don't allow filtering in similar // No-op: we don't allow filtering in similar
} }
override fun onItemLongClick(position: Int) {
return
}
override fun onAddPageError(error: Throwable) {
super.onAddPageError(error)
binding.emptyView.show(activity!!.getString(R.string.similar_no_results))
}
companion object { companion object {
const val MANGA_ID = "manga_id" const val MANGA_ID = "manga_id"
const val MANGA_TITLE = "manga_title" const val MANGA_TITLE = "manga_title"

View File

@ -1,19 +1,20 @@
package exh.md.similar package exh.md.similar
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
/** /**
* MangaDexSimilarPager inherited from the general Pager. * MangaDexSimilarPagingSource inherited from the general Pager.
*/ */
class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() { class MangaDexSimilarPagingSource(val manga: Manga, val source: MangaDex) : BrowsePagingSource() {
override suspend fun requestNextPage() { override suspend fun requestNextPage(currentPage: Int): MangasPage {
val mangasPage = coroutineScope { val mangasPage = coroutineScope {
val similarPageDef = async { source.getMangaSimilar(manga.toSManga()) } val similarPageDef = async { source.getMangaSimilar(manga.toSManga()) }
val relatedPageDef = async { source.getMangaRelated(manga.toSManga()) } val relatedPageDef = async { source.getMangaRelated(manga.toSManga()) }
@ -27,10 +28,6 @@ class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
) )
} }
if (mangasPage.mangas.isNotEmpty()) { return mangasPage.takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException()
onPageReceived(mangasPage)
} else {
throw NoResultsException()
}
} }
} }

View File

@ -1,11 +1,18 @@
package exh.md.similar package exh.md.similar
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.paging.PagingSource
import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
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.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
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 import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource import exh.source.getMainSource
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -22,9 +29,17 @@ class MangaDexSimilarPresenter(
var manga: Manga? = null var manga: Manga? = null
override fun createPager(query: String, filters: FilterList): Pager { override fun onCreate(savedState: Bundle?) {
val sourceAsMangaDex = source.getMainSource() as MangaDex super.onCreate(savedState)
this.manga = runBlocking { getManga.await(mangaId) } this.manga = runBlocking { getManga.await(mangaId) }
return MangaDexSimilarPager(manga!!, sourceAsMangaDex) }
override fun createPager(query: String, filters: FilterList): PagingSource<Long, Pair<SManga, RaisedSearchMetadata?>> {
return MangaDexSimilarPagingSource(manga!!, source!!.getMainSource() as MangaDex)
}
@Composable
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
return remember { mutableStateOf(initialMetadata) }
} }
} }

View File

@ -1,16 +1,14 @@
package exh.recs package exh.recs
import android.os.Bundle import android.os.Bundle
import android.view.Menu import androidx.compose.runtime.Composable
import android.view.View
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R import eu.kanade.presentation.browse.BrowseRecommendationsScreen
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.SourcesController import eu.kanade.tachiyomi.ui.browse.source.SourcesController
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
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
@ -24,31 +22,26 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
), ),
) )
override fun getTitle(): String? {
return (presenter as? RecommendsPresenter)?.manga?.title
}
override fun createPresenter(): RecommendsPresenter { override fun createPresenter(): RecommendsPresenter {
return RecommendsPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY)) return RecommendsPresenter(args.getLong(MANGA_ID), 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 BrowseRecommendationsScreen(
menu.findItem(R.id.action_open_in_web_view).isVisible = false presenter = presenter,
menu.findItem(R.id.action_settings).isVisible = false navigateUp = { router.popCurrentController() },
title = (presenter as RecommendsPresenter).manga!!.title,
onMangaClick = { manga ->
openSmartSearch(manga.ogTitle)
},
)
} }
override fun initFilterSheet() { override fun initFilterSheet() {
// No-op: we don't allow filtering in recs // No-op: we don't allow filtering in recs
} }
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
openSmartSearch(item.manga.ogTitle)
return true
}
private fun openSmartSearch(title: String) { private fun openSmartSearch(title: String) {
val smartSearchConfig = SourcesController.SmartSearchConfig(title) val smartSearchConfig = SourcesController.SmartSearchConfig(title)
router.pushController( router.pushController(
@ -60,10 +53,6 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
) )
} }
override fun onItemLongClick(position: Int) {
return
}
companion object { companion object {
const val MANGA_ID = "manga_id" const val MANGA_ID = "manga_id"
} }

View File

@ -8,12 +8,13 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import exh.util.MangaType import exh.util.MangaType
import exh.util.mangaType import exh.util.mangaType
import exh.util.nullIfEmpty
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
@ -191,12 +192,12 @@ class Anilist : API("https://graphql.anilist.co/") {
} }
} }
open class RecommendsPager( open class RecommendsPagingSource(
private val manga: Manga, private val manga: Manga,
private val smart: Boolean = true, private val smart: Boolean = true,
private var preferredApi: API = API.MYANIMELIST, private var preferredApi: API = API.MYANIMELIST,
) : Pager() { ) : BrowsePagingSource() {
override suspend fun requestNextPage() { override suspend fun requestNextPage(currentPage: Int): MangasPage {
if (smart) preferredApi = if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi if (smart) preferredApi = if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi
val apiList = API_MAP.toList().sortedByDescending { it.first == preferredApi } val apiList = API_MAP.toList().sortedByDescending { it.first == preferredApi }
@ -210,15 +211,9 @@ open class RecommendsPager(
logcat(LogPriority.ERROR, e) { key.toString() } logcat(LogPriority.ERROR, e) { key.toString() }
null null
} }
}.orEmpty() }?.nullIfEmpty() ?: throw NoResultsException()
val mangasPage = MangasPage(recs, false) return MangasPage(recs, false)
if (mangasPage.mangas.isNotEmpty()) {
onPageReceived(mangasPage)
} else {
throw NoResultsException()
}
} }
companion object { companion object {

View File

@ -1,10 +1,13 @@
package exh.recs package exh.recs
import android.os.Bundle
import androidx.paging.PagingSource
import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
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 import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -20,8 +23,12 @@ class RecommendsPresenter(
var manga: Manga? = null var manga: Manga? = null
override fun createPager(query: String, filters: FilterList): Pager { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
this.manga = runBlocking { getManga.await(mangaId) } this.manga = runBlocking { getManga.await(mangaId) }
return RecommendsPager(manga!!) }
override fun createPager(query: String, filters: FilterList): PagingSource<Long, Pair<SManga, RaisedSearchMetadata?>> {
return RecommendsPagingSource(manga!!)
} }
} }

View File

@ -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>

View File

@ -1,78 +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" />
<FrameLayout
android:id="@+id/play_layout"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="bottom|end"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/thumbnail"
app:layout_constraintEnd_toEndOf="@+id/thumbnail"
tools:visibility="visible">
<ImageView
android:id="@+id/play_button"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:layout_marginBottom="6dp"
android:background="@drawable/round_play_background"
android:contentDescription="@string/action_start_reading"
android:padding="6dp"
android:src="@drawable/ic_start_reading_24dp"
app:tint="@android:color/white" />
</FrameLayout>
<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>

View File

@ -1,86 +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" />
<FrameLayout
android:id="@+id/play_layout"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="end"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/thumbnail"
app:layout_constraintTop_toBottomOf="@+id/badges"
tools:visibility="visible">
<ImageView
android:id="@+id/play_button"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:layout_marginBottom="6dp"
android:background="@drawable/round_play_background"
android:contentDescription="@string/action_start_reading"
android:padding="6dp"
android:src="@drawable/ic_start_reading_24dp"
app:tint="@android:color/white" />
</FrameLayout>
<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>

View File

@ -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>

View File

@ -1,117 +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="148dp"
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="100dp"
android:layout_height="140dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="w,2:3"
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" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/thumbnail"
app:layout_constraintTop_toTopOf="parent"
tools:text="Manga title for the life of me I cant think yes totally" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/uploader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/thumbnail"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Manga title for the life of me I cant think yes totally" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/rounded_rectangle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/thumbnail">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/genre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:textAppearance="?attr/textAppearanceBodyMedium" />
</com.google.android.material.card.MaterialCardView>
<me.zhanghai.android.materialratingbar.MaterialRatingBar
android:id="@+id/rating_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:isIndicator="true"
android:maxHeight="20dp"
android:minHeight="20dp"
android:numStars="5"
app:layout_constraintBottom_toTopOf="@+id/cardView"
app:layout_constraintStart_toEndOf="@+id/thumbnail" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_posted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:maxLines="1"
app:layout_constraintBottom_toTopOf="@+id/date_posted"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -1,57 +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:id="@+id/action_display_mode"
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_no_title_grid"
android:title="@string/action_display_no_title_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_settings"
android:icon="@drawable/ic_settings_24dp"
android:title="@string/action_settings"
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>

View File

@ -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>

View File

@ -14,3 +14,4 @@ debugOverlay-standard = { module = "com.ms-square:debugoverlay", version.ref = "
debugOverlay-noop = { module = "com.ms-square:debugoverlay-no-op", version.ref = "debugOverlay" } debugOverlay-noop = { module = "com.ms-square:debugoverlay-no-op", version.ref = "debugOverlay" }
ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0" ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0"
composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.2.3"