Use Voyager on BrowseSource and SourceSearch screen (#8650)

Some navigation janks will be dealt with when the migration is complete

(cherry picked from commit 94d1b68598692cc0ef981e2dfbf12303fa962f63)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt
#	app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt
#	app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt
#	app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt
#	app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.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/BrowseSourceScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
This commit is contained in:
Ivan Iskandar 2022-12-01 11:05:11 +07:00 committed by Jobobby04
parent 6185c95715
commit 6402258c83
37 changed files with 1396 additions and 1231 deletions

View File

@ -22,7 +22,7 @@ class MangaMetadataRepositoryImpl(
return handler.awaitOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) } return handler.awaitOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) }
} }
override suspend fun subscribeMetadataById(id: Long): Flow<SearchMetadata?> { override fun subscribeMetadataById(id: Long): Flow<SearchMetadata?> {
return handler.subscribeToOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) } return handler.subscribeToOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) }
} }
@ -30,7 +30,7 @@ class MangaMetadataRepositoryImpl(
return handler.awaitList { search_tagsQueries.selectByMangaId(id, searchTagMapper) } return handler.awaitList { search_tagsQueries.selectByMangaId(id, searchTagMapper) }
} }
override suspend fun subscribeTagsById(id: Long): Flow<List<SearchTag>> { override fun subscribeTagsById(id: Long): Flow<List<SearchTag>> {
return handler.subscribeToList { search_tagsQueries.selectByMangaId(id, searchTagMapper) } return handler.subscribeToList { search_tagsQueries.selectByMangaId(id, searchTagMapper) }
} }
@ -38,7 +38,7 @@ class MangaMetadataRepositoryImpl(
return handler.awaitList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) } return handler.awaitList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) }
} }
override suspend fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>> { override fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>> {
return handler.subscribeToList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) } return handler.subscribeToList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) }
} }

View File

@ -29,7 +29,7 @@ class GetFlatMetadataById(
} }
} }
suspend fun subscribe(id: Long): Flow<FlatMetadata?> { fun subscribe(id: Long): Flow<FlatMetadata?> {
return combine( return combine(
mangaMetadataRepository.subscribeMetadataById(id), mangaMetadataRepository.subscribeMetadataById(id),
mangaMetadataRepository.subscribeTagsById(id), mangaMetadataRepository.subscribeTagsById(id),

View File

@ -11,15 +11,15 @@ import kotlinx.coroutines.flow.Flow
interface MangaMetadataRepository { interface MangaMetadataRepository {
suspend fun getMetadataById(id: Long): SearchMetadata? suspend fun getMetadataById(id: Long): SearchMetadata?
suspend fun subscribeMetadataById(id: Long): Flow<SearchMetadata?> fun subscribeMetadataById(id: Long): Flow<SearchMetadata?>
suspend fun getTagsById(id: Long): List<SearchTag> suspend fun getTagsById(id: Long): List<SearchTag>
suspend fun subscribeTagsById(id: Long): Flow<List<SearchTag>> fun subscribeTagsById(id: Long): Flow<List<SearchTag>>
suspend fun getTitlesById(id: Long): List<SearchTitle> suspend fun getTitlesById(id: Long): List<SearchTitle>
suspend fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>> fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>>
suspend fun insertFlatMetadata(flatMetadata: FlatMetadata) suspend fun insertFlatMetadata(flatMetadata: FlatMetadata)

View File

@ -1,68 +0,0 @@
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.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.library.model.LibraryDisplayMode
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.ui.browse.source.browse.BrowseSourcePresenter
@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() }
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
title = stringResource(R.string.mangadex_follows),
displayMode = presenter.displayMode,
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
state = presenter,
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 = null,
onHelpClick = null,
onLocalSourceHelpClick = null,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}

View File

@ -1,56 +0,0 @@
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.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.ui.browse.source.browse.BrowseSourcePresenter
@Composable
fun BrowseRecommendationsScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
title: String,
onMangaClick: (Manga) -> Unit,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
navigateUp = navigateUp,
title = title,
displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it },
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
BrowseSourceContent(
state = presenter,
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 = null,
onHelpClick = null,
onLocalSourceHelpClick = null,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaClick,
)
}
}

View File

@ -1,250 +1,44 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.data.source.NoResultsException import eu.kanade.data.source.NoResultsException
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceEHentaiList import eu.kanade.presentation.browse.components.BrowseSourceEHentaiList
import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BrowseSourceScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
openFilterSheet: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
// SY -->
onSettingsClick: () -> Unit,
// SY <--
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
Scaffold(
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar(
state = presenter,
source = presenter.source,
displayMode = presenter.displayMode.takeUnless { presenter.source!!.isEhBasedSource() && presenter.ehentaiBrowseDisplayMode },
onDisplayModeChange = { presenter.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { presenter.search(it) },
// SY -->
onSettingsClick = onSettingsClick,
// SY <--
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Popular,
onClick = {
presenter.reset()
presenter.search(GetRemoteManga.QUERY_POPULAR)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Favorite,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.popular))
},
)
if (presenter.source?.supportsLatest == true) {
FilterChip(
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Latest,
onClick = {
presenter.reset()
presenter.search(GetRemoteManga.QUERY_LATEST)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.NewReleases,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.latest))
},
)
}
/* SY --> if (presenter.filters.isNotEmpty())*/ run /* SY <-- */ {
FilterChip(
selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
onClick = openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
// SY -->
Text(
text = if (presenter.filters.isNotEmpty()) {
stringResource(R.string.action_filter)
} else {
stringResource(R.string.action_search)
},
)
// SY <--
},
)
}
}
Divider()
AppStateBanners(downloadedOnlyMode, incognitoMode)
}
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
state = presenter,
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 BrowseSourceFloatingActionButton(
modifier: Modifier = Modifier.navigationBarsPadding(),
isVisible: Boolean,
onFabClick: () -> Unit,
) {
run {
ExtendedFloatingActionButton(
modifier = modifier,
text = {
Text(
text = if (isVisible) {
stringResource(R.string.action_filter)
} else {
stringResource(R.string.saved_searches)
},
)
},
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
)
}
}
@Composable @Composable
fun BrowseSourceContent( fun BrowseSourceContent(
state: BrowseSourceState, source: CatalogueSource?,
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
columns: GridCells, columns: GridCells,
// SY -->
ehentaiBrowseDisplayMode: Boolean, ehentaiBrowseDisplayMode: Boolean,
// SY <--
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
@ -287,7 +81,7 @@ fun BrowseSourceContent(
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
EmptyScreen( EmptyScreen(
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actions = if (state.source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) { actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
listOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
@ -335,11 +129,9 @@ fun BrowseSourceContent(
} }
// SY --> // SY -->
if (state.source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) { if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
BrowseSourceEHentaiList( BrowseSourceEHentaiList(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
getMetadataState = getMetadataState,
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, onMangaLongClick = onMangaLongClick,
@ -352,10 +144,6 @@ fun BrowseSourceContent(
LibraryDisplayMode.ComfortableGrid -> { LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid( BrowseSourceComfortableGrid(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
// SY -->
getMetadataState = getMetadataState,
// SY <--
columns = columns, columns = columns,
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
@ -365,22 +153,14 @@ fun BrowseSourceContent(
LibraryDisplayMode.List -> { LibraryDisplayMode.List -> {
BrowseSourceList( BrowseSourceList(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
// SY -->
getMetadataState = getMetadataState,
// SY <--
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, onMangaLongClick = onMangaLongClick,
) )
} }
else -> { LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
BrowseSourceCompactGrid( BrowseSourceCompactGrid(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
// SY -->
getMetadataState = getMetadataState,
// SY <--
columns = columns, columns = columns,
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,

View File

@ -1,41 +0,0 @@
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.BrowseSourcePresenter.Filter
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
@Stable
interface BrowseSourceState {
val source: CatalogueSource?
var searchQuery: String?
val currentFilter: Filter
val isUserQuery: Boolean
val filters: FilterList
val filterItems: List<IFlexible<*>>
var dialog: BrowseSourcePresenter.Dialog?
}
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
return when (val filter = Filter.valueOf(initialQuery ?: "")) {
Filter.Latest, Filter.Popular -> BrowseSourceStateImpl(initialCurrentFilter = filter)
is Filter.UserInput -> BrowseSourceStateImpl(initialQuery = initialQuery, initialCurrentFilter = filter)
}
}
class BrowseSourceStateImpl(initialQuery: String? = null, initialCurrentFilter: Filter) : BrowseSourceState {
override var source: CatalogueSource? by mutableStateOf(null)
override var searchQuery: String? by mutableStateOf(initialQuery)
override var currentFilter: Filter by mutableStateOf(initialCurrentFilter)
override val isUserQuery: Boolean by derivedStateOf { currentFilter is Filter.UserInput && currentFilter.query.isNotEmpty() }
override var filters: FilterList by mutableStateOf(FilterList())
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
}

View File

@ -26,6 +26,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold

View File

@ -1,80 +0,0 @@
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.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
@Composable
fun SourceSearchScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onFabClick: () -> Unit,
onMangaClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
Scaffold(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = presenter.searchQuery ?: "",
onChangeSearchQuery = { presenter.searchQuery = it },
onClickCloseSearch = navigateUp,
onSearch = { presenter.search(it) },
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
BrowseSourceFloatingActionButton(
isVisible = presenter.filters.isNotEmpty(),
onFabClick = onFabClick,
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
state = presenter,
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 = onMangaClick,
)
}
}

View File

@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -25,14 +25,11 @@ import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BrowseSourceComfortableGrid( fun BrowseSourceComfortableGrid(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
columns: GridCells, columns: GridCells,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
@ -51,10 +48,10 @@ fun BrowseSourceComfortableGrid(
} }
items(mangaList.itemCount) { index -> items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items
val manga by getMangaState(initialManga.first)
// SY --> // SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second) val pair by mangaList[index]?.collectAsState() ?: return@items
val manga = pair.first
val metadata = pair.second
// SY <-- // SY <--
BrowseSourceComfortableGridItem( BrowseSourceComfortableGridItem(
manga = manga, manga = manga,

View File

@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -25,14 +25,11 @@ import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BrowseSourceCompactGrid( fun BrowseSourceCompactGrid(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
columns: GridCells, columns: GridCells,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
@ -51,10 +48,10 @@ fun BrowseSourceCompactGrid(
} }
items(mangaList.itemCount) { index -> items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items
val manga by getMangaState(initialManga.first)
// SY --> // SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second) val pair by mangaList[index]?.collectAsState() ?: return@items
val manga = pair.first
val metadata = pair.second
// SY <-- // SY <--
BrowseSourceCompactGridItem( BrowseSourceCompactGridItem(
manga = manga, manga = manga,

View File

@ -1,12 +1,23 @@
package eu.kanade.presentation.browse.components package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.DialogProperties
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.toast
@Composable @Composable
fun RemoveMangaDialog( fun RemoveMangaDialog(
@ -39,3 +50,105 @@ fun RemoveMangaDialog(
}, },
) )
} }
@Composable
fun FailedToLoadSavedSearchDialog(
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.ok))
}
},
title = {
Text(text = stringResource(R.string.save_search_failed_to_load))
},
text = {
Text(text = stringResource(R.string.save_search_failed_to_load_message))
},
)
}
@Composable
fun SavedSearchDeleteDialog(
onDismissRequest: () -> Unit,
name: String,
deleteSavedSearch: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
dismissButton = {
TextButton(
onClick = {
deleteSavedSearch()
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
title = {
Text(text = stringResource(R.string.save_search_delete))
},
text = {
Text(text = stringResource(R.string.save_search_delete_message, name))
},
)
}
@Composable
fun SavedSearchCreateDialog(
onDismissRequest: () -> Unit,
currentSavedSearches: List<String>,
saveSearch: (String) -> Unit,
) {
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(""))
}
val context = LocalContext.current
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.save_search)) },
text = {
OutlinedTextField(
value = textFieldValue,
onValueChange = { textFieldValue = it },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(text = stringResource(R.string.save_search_hint))
},
)
},
properties = DialogProperties(
usePlatformDefaultWidth = true,
),
confirmButton = {
TextButton(
onClick = {
val searchName = textFieldValue.text.trim()
if (searchName.isNotBlank() && searchName !in currentSavedSearches) {
saveSearch(searchName)
onDismissRequest()
} else {
context.toast(R.string.save_search_invalid_name)
}
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
)
}

View File

@ -16,7 +16,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -46,15 +46,12 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.util.SourceTagsUtil import exh.util.SourceTagsUtil
import exh.util.SourceTagsUtil.GenreColor import exh.util.SourceTagsUtil.GenreColor
import exh.util.floor import exh.util.floor
import kotlinx.coroutines.flow.StateFlow
import java.util.Date import java.util.Date
@Composable @Composable
fun BrowseSourceEHentaiList( fun BrowseSourceEHentaiList(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit,
@ -69,10 +66,10 @@ fun BrowseSourceEHentaiList(
} }
items(mangaList) { initialManga -> items(mangaList) { initialManga ->
initialManga ?: return@items val pair by initialManga?.collectAsState() ?: return@items
val manga by getMangaState(initialManga.first) val manga = pair.first
// SY --> // SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second) val metadata = pair.second
// SY <-- // SY <--
BrowseSourceEHentaiListItem( BrowseSourceEHentaiListItem(
manga = manga, manga = manga,

View File

@ -0,0 +1,33 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.tachiyomi.R
@Composable
fun BrowseSourceFloatingActionButton(
modifier: Modifier = Modifier,
isVisible: Boolean,
onFabClick: () -> Unit,
) {
ExtendedFloatingActionButton(
modifier = modifier,
text = {
Text(
text = if (isVisible) {
stringResource(R.string.action_filter)
} else {
stringResource(R.string.saved_searches)
},
)
},
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
)
}

View File

@ -3,7 +3,7 @@ package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -23,14 +23,11 @@ import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BrowseSourceList( fun BrowseSourceList(
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
// SY -->
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
// SY <--
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit,
@ -44,12 +41,14 @@ fun BrowseSourceList(
} }
} }
items(mangaList) { initialManga -> items(mangaList) { mangaflow ->
initialManga ?: return@items mangaflow ?: return@items
val manga by getMangaState(initialManga.first)
// SY --> // SY -->
val metadata by getMetadataState(initialManga.first, initialManga.second) val pair by mangaflow.collectAsState()
val manga = pair.first
val metadata = pair.second
// SY <-- // SY <--
BrowseSourceListItem( BrowseSourceListItem(
manga = manga, manga = manga,
// SY --> // SY -->

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.components.AppBarTitle
@ -29,7 +28,8 @@ import exh.source.anyIs
@Composable @Composable
fun BrowseSourceToolbar( fun BrowseSourceToolbar(
state: BrowseSourceState, searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
source: CatalogueSource?, source: CatalogueSource?,
displayMode: LibraryDisplayMode?, displayMode: LibraryDisplayMode?,
onDisplayModeChange: (LibraryDisplayMode) -> Unit, onDisplayModeChange: (LibraryDisplayMode) -> Unit,
@ -49,8 +49,8 @@ fun BrowseSourceToolbar(
SearchToolbar( SearchToolbar(
navigateUp = navigateUp, navigateUp = navigateUp,
titleContent = { AppBarTitle(title) }, titleContent = { AppBarTitle(title) },
searchQuery = state.searchQuery, searchQuery = searchQuery,
onChangeSearchQuery = { state.searchQuery = it }, onChangeSearchQuery = onSearchQueryChange,
onSearch = onSearch, onSearch = onSearch,
onClickCloseSearch = navigateUp, onClickCloseSearch = navigateUp,
actions = { actions = {

View File

@ -59,23 +59,3 @@ fun SourceFeedDeleteDialog(
}, },
) )
} }
@Composable
fun SourceFeedFailedToLoadSavedSearchDialog(
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.ok))
}
},
title = {
Text(text = stringResource(R.string.save_search_failed_to_load))
},
text = {
Text(text = stringResource(R.string.save_search_failed_to_load_message))
},
)
}

View File

@ -3,14 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrateSearchScreen import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
@ -19,14 +16,8 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId, validSources = validSources) } val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId, validSources = validSources) }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
// SY -->
val migrationScreen = remember {
navigator.items.filterIsInstance<MigrationListScreen>().last()
}
// SY <--
MigrateSearchScreen( MigrateSearchScreen(
navigateUp = navigator::pop, navigateUp = navigator::pop,
@ -41,21 +32,16 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
screenModel.lastUsedSourceId.set(it.id) screenModel.lastUsedSourceId.set(it.id)
} }
// SY --> // SY -->
router.pushController( navigator.push(SourceSearchScreen(state.manga!!, it.id, state.searchQuery))
SourceSearchController(state.manga!!, it, state.searchQuery)
.also { searchController ->
searchController.useMangaForMigration = { newMangaId ->
migrationScreen.newSelectedItem = mangaId to newMangaId
navigator.pop()
}
},
)
// SY <-- // SY <--
}, },
onClickItem = { onClickItem = {
// SY --> // SY -->
migrationScreen.newSelectedItem = mangaId to it.id navigator.items
navigator.pop() .filterIsInstance<MigrationListScreen>()
.last()
.newSelectedItem = mangaId to it.id
navigator.popUntil { it is MigrationListScreen }
// SY <-- // SY <--
}, },
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },

View File

@ -2,18 +2,14 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
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.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.util.system.getSerializableCompat
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
class SourceSearchController( class SourceSearchController(bundle: Bundle) : BasicFullComposeController(bundle) {
bundle: Bundle,
) : BrowseSourceController(bundle) {
constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this( constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
bundleOf( bundleOf(
@ -23,33 +19,16 @@ class SourceSearchController(
), ),
) )
var useMangaForMigration: ((Long) -> Unit)? = null private var oldManga: Manga = args.getSerializableCompat(MANGA_KEY)!!
private val sourceId = args.getLong(SOURCE_ID_KEY)
private val query = args.getString(SEARCH_QUERY_KEY)
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
SourceSearchScreen( Navigator(screen = SourceSearchScreen(oldManga, sourceId, query))
presenter = presenter,
navigateUp = { router.popCurrentController() },
onFabClick = { filterSheet?.show() },
// SY -->
onMangaClick = { manga ->
useMangaForMigration?.let { it(manga.id) }
router.popCurrentController()
},
// SY <--
onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
activity?.let { context ->
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
},
)
LaunchedEffect(presenter.filters) {
initFilterSheet()
}
} }
} }
private const val MANGA_KEY = "oldManga" private const val MANGA_KEY = "oldManga"
private const val SOURCE_ID_KEY = "sourceId"
private const val SEARCH_QUERY_KEY = "searchQuery"

View File

@ -0,0 +1,114 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
data class SourceSearchScreen(
private val oldManga: Manga,
private val sourceId: Long,
private val query: String? = null,
) : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = state.toolbarQuery ?: "",
onChangeSearchQuery = screenModel::setToolbarQuery,
onClickCloseSearch = navigateUp,
onSearch = { screenModel.search(it) },
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
// SY -->
BrowseSourceFloatingActionButton(
isVisible = state.filters.isNotEmpty(),
onFabClick = screenModel::openFilterSheet,
)
// SY <--
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
val openMigrateDialog: (Manga) -> Unit = {
// SY -->
navigator.items
.filterIsInstance<MigrationListScreen>()
.last()
.newSelectedItem = oldManga.id to it.id
navigator.popUntil { it is MigrationListScreen }
// SY <--
}
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
// SY -->
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
// SY <--
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = {
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
},
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) },
onMangaClick = openMigrateDialog,
onMangaLongClick = openMigrateDialog,
)
}
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context, router)
}
}
}

View File

@ -68,7 +68,7 @@ fun Screen.sourcesTab(
val controller = when { val controller = when {
smartSearchConfig != null -> SmartSearchController(source.id, smartSearchConfig) smartSearchConfig != null -> SmartSearchController(source.id, smartSearchConfig)
(query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedController(source.id) (query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedController(source.id)
else -> BrowseSourceController(source, query) else -> BrowseSourceController(source.id, query)
} }
screenModel.onOpenSource(source) screenModel.onOpenSource(source)
router.pushController(controller) router.pushController(controller)

View File

@ -1,39 +1,20 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import cafe.adriel.voyager.navigator.CurrentScreen
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import cafe.adriel.voyager.navigator.Navigator
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
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.browse.BrowseSourcePresenter.Dialog import kotlinx.coroutines.channels.Channel
import eu.kanade.tachiyomi.ui.category.CategoryController import kotlinx.coroutines.flow.collectLatest
import eu.kanade.tachiyomi.ui.manga.MangaController import kotlinx.coroutines.flow.consumeAsFlow
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import kotlinx.coroutines.launch
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
import exh.savedsearches.EXHSavedSearch
open class BrowseSourceController(bundle: Bundle) : class BrowseSourceController(bundle: Bundle) : BasicFullComposeController(bundle) {
FullComposeController<BrowseSourcePresenter>(bundle) {
constructor( constructor(
sourceId: Long, sourceId: Long,
@ -98,200 +79,41 @@ open class BrowseSourceController(bundle: Bundle) :
filterList, filterList,
) )
/** private val sourceId = args.getLong(SOURCE_ID_KEY)
* Sheet containing filter items. private val initialQuery = args.getString(SEARCH_QUERY_KEY)
*/
protected var filterSheet: SourceFilterSheet? = null
override fun createPresenter(): BrowseSourcePresenter { // SY -->
// SY --> private val filtersJson = args.getString(FILTERS_CONFIG_KEY)
return BrowseSourcePresenter( private val savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L }
args.getLong(SOURCE_ID_KEY), // SY <--
args.getString(SEARCH_QUERY_KEY),
filtersJson = args.getString(FILTERS_CONFIG_KEY), private val queryEvent = Channel<BrowseSourceScreen.SearchType>()
savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L },
)
// SY <--
}
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
val scope = rememberCoroutineScope() Navigator(
val context = LocalContext.current screen = BrowseSourceScreen(
val haptic = LocalHapticFeedback.current sourceId = sourceId,
query = initialQuery,
// SY -->
filtersJson = filtersJson,
savedSearch = savedSearch,
// SY <--
),
) { navigator ->
CurrentScreen()
BrowseSourceScreen( LaunchedEffect(Unit) {
presenter = presenter, queryEvent.consumeAsFlow()
navigateUp = ::navigateUp, .collectLatest {
openFilterSheet = { filterSheet?.show() }, val screen = (navigator.lastItem as? BrowseSourceScreen)
onMangaClick = { router.pushController(MangaController(it.id, true)) }, when (it) {
onMangaLongClick = { manga -> is BrowseSourceScreen.SearchType.Genre -> screen?.searchGenre(it.txt)
scope.launchIO { is BrowseSourceScreen.SearchType.Text -> screen?.search(it.txt)
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)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
},
onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
},
// SY -->
onSettingsClick = {
router.pushController(SourcePreferencesController(presenter.source!!.id))
},
// SY <--
incognitoMode = presenter.isIncognitoMode,
downloadedOnlyMode = presenter.isDownloadOnly,
)
val onDismissRequest = { presenter.dialog = null }
when (val dialog = presenter.dialog) {
null -> {}
is Dialog.Migrate -> {}
is Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { presenter.addFavorite(dialog.manga) },
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
)
}
is Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
presenter.changeMangaFavorite(dialog.manga)
},
mangaToRemove = 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)
},
)
}
}
BackHandler(onBack = ::navigateUp)
LaunchedEffect(presenter.filters) {
initFilterSheet()
}
}
private fun navigateUp() {
when {
!presenter.isUserQuery && presenter.searchQuery != null -> presenter.searchQuery = null
else -> router.popCurrentController()
}
}
fun setSavedSearches(savedSearches: List<EXHSavedSearch>) {
filterSheet?.setSavedSearches(savedSearches)
}
open fun initFilterSheet() {
filterSheet = SourceFilterSheet(
activity!!,
// SY -->
router,
presenter.source!!,
emptyList(),
// SY <--
onFilterClicked = {
presenter.search(filters = presenter.filters)
},
onResetClicked = {
filterSheet?.dismiss()
presenter.reset()
},
// EXH -->
onSaveClicked = {
viewScope.launchUI {
filterSheet?.context?.let {
val names = presenter.loadSearches().map { it.name }
var searchName = ""
MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search)
.setTextInput(hint = it.getString(R.string.save_search_hint)) { input ->
searchName = input
}
.setPositiveButton(R.string.action_save) { _, _ ->
if (searchName.isNotBlank() && searchName !in names) {
presenter.saveSearch(
name = searchName.trim(),
query = presenter.currentFilter.query,
filterList = presenter.currentFilter.filters.ifEmpty { presenter.source!!.getFilterList() },
)
} else {
it.toast(R.string.save_search_invalid_name)
}
}
.setNegativeButton(R.string.action_cancel, null)
.show()
}
}
},
onSavedSearchClicked = { idOfSearch ->
viewScope.launchUI {
val search = presenter.loadSearch(idOfSearch)
if (search == null) {
filterSheet?.context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search_failed_to_load)
.setMessage(R.string.save_search_failed_to_load_message)
.show()
} }
return@launchUI
} }
}
if (search.filterList == null && presenter.filters.isNotEmpty()) {
activity?.toast(R.string.save_search_invalid)
return@launchUI
}
val allDefault = search.filterList != null && presenter.filters == presenter.source!!.getFilterList()
filterSheet?.dismiss()
presenter.search(
query = search.query,
filters = if (allDefault) null else search.filterList,
)
}
},
onSavedSearchDeleteClicked = { idToDelete, name ->
filterSheet?.context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search_delete)
.setMessage(it.getString(R.string.save_search_delete_message, name))
.setPositiveButton(R.string.action_cancel, null)
.setNegativeButton(android.R.string.ok) { _, _ ->
presenter.deleteSearch(idToDelete)
}
.show()
}
},
// EXH <--
)
launchUI {
filterSheet?.setSavedSearches(presenter.loadSearches())
} }
filterSheet?.setFilters(presenter.filterItems)
} }
/** /**
@ -300,7 +122,7 @@ open class BrowseSourceController(bundle: Bundle) :
* @param newQuery the new query. * @param newQuery the new query.
*/ */
fun searchWithQuery(newQuery: String) { fun searchWithQuery(newQuery: String) {
presenter.search(newQuery) viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Text(newQuery)) }
} }
/** /**
@ -311,52 +133,15 @@ 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() viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Genre(genreName)) }
var genreExists = false
filter@ for (sourceFilter in defaultFilters) {
if (sourceFilter is Filter.Group<*>) {
for (filter in sourceFilter.state) {
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
when (filter) {
is Filter.TriState -> filter.state = 1
is Filter.CheckBox -> filter.state = true
else -> {}
}
genreExists = true
break@filter
}
}
} else if (sourceFilter is Filter.Select<*>) {
val index = sourceFilter.values.filterIsInstance<String>()
.indexOfFirst { it.equals(genreName, true) }
if (index != -1) {
sourceFilter.state = index
genreExists = true
break
}
}
}
if (genreExists) {
filterSheet?.setFilters(defaultFilters.toItems())
presenter.search(filters = defaultFilters)
} else {
searchWithQuery(genreName)
}
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
const val SEARCH_QUERY_KEY = "searchQuery"
// SY -->
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
const val SAVED_SEARCH_CONFIG_KEY = "savedSearch"
const val FILTERS_CONFIG_KEY = "filters"
// SY <--
} }
} }
private const val SOURCE_ID_KEY = "sourceId"
private const val SEARCH_QUERY_KEY = "searchQuery"
// SY -->
private const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
private const val SAVED_SEARCH_CONFIG_KEY = "savedSearch"
private const val FILTERS_CONFIG_KEY = "filters"
// SY <--

View File

@ -0,0 +1,326 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.browse.components.FailedToLoadSavedSearchDialog
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.browse.components.SavedSearchCreateDialog
import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.DuplicateMangaDialog
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
data class BrowseSourceScreen(
private val sourceId: Long,
private val query: String? = null,
// SY -->
private val filtersJson: String? = null,
private val savedSearch: Long? = null,
// SY <--
) : Screen {
override val key = uniqueScreenKey
@Composable
override fun Content() {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val screenModel = rememberScreenModel {
BrowseSourceScreenModel(
sourceId = sourceId,
searchQuery = query,
// SY -->
filtersJson = filtersJson,
savedSearch = savedSearch,
// SY <--
)
}
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
val onWebViewClick = f@{
val source = screenModel.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar(
searchQuery = state.toolbarQuery,
onSearchQueryChange = screenModel::setToolbarQuery,
source = screenModel.source,
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { screenModel.search(it) },
// SY -->
onSettingsClick = { router.pushController(SourcePreferencesController(sourceId)) },
// SY <--
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = state.currentFilter == BrowseSourceScreenModel.Filter.Popular,
onClick = {
screenModel.reset()
screenModel.search(GetRemoteManga.QUERY_POPULAR)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Favorite,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.popular))
},
)
if (screenModel.source.supportsLatest) {
FilterChip(
selected = state.currentFilter == BrowseSourceScreenModel.Filter.Latest,
onClick = {
screenModel.reset()
screenModel.search(GetRemoteManga.QUERY_LATEST)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.NewReleases,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.latest))
},
)
}
/* SY --> if (state.filters.isNotEmpty())*/ run /* SY <-- */ {
FilterChip(
selected = state.currentFilter is BrowseSourceScreenModel.Filter.UserInput,
onClick = screenModel::openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
// SY -->
Text(
text = if (state.filters.isNotEmpty()) {
stringResource(R.string.action_filter)
} else {
stringResource(R.string.action_search)
},
)
// SY <--
},
)
}
}
Divider()
AppStateBanners(screenModel.isDownloadOnly, screenModel.isIncognitoMode)
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
// SY -->
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
// SY <--
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = { router.pushController(MangaController(it.id, true)) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
)
else -> screenModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
},
)
}
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
duplicateFrom = screenModel.getSourceOrStub(dialog.duplicate),
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = {
router.pushController(CategoryController())
},
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
},
)
}
is BrowseSourceScreenModel.Dialog.CreateSavedSearh -> SavedSearchCreateDialog(
onDismissRequest = onDismissRequest,
currentSavedSearches = dialog.currentSavedSearches,
saveSearch = screenModel::saveSearch,
)
is BrowseSourceScreenModel.Dialog.DeleteSavedSearch -> SavedSearchDeleteDialog(
onDismissRequest = onDismissRequest,
name = dialog.name,
deleteSavedSearch = {
screenModel.deleteSearch(dialog.idToDelete)
},
)
BrowseSourceScreenModel.Dialog.FailedToLoadSavedSearch -> FailedToLoadSavedSearchDialog(onDismissRequest)
else -> {}
}
BackHandler(onBack = navigateUp)
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context, router)
}
LaunchedEffect(Unit) {
queryEvent.receiveAsFlow()
.collectLatest {
when (it) {
is SearchType.Genre -> screenModel.searchGenre(it.txt)
is SearchType.Text -> screenModel.search(it.txt)
}
}
}
}
private val queryEvent = Channel<SearchType>()
suspend fun search(query: String) = queryEvent.send(SearchType.Text(query))
suspend fun searchGenre(name: String) = queryEvent.send(SearchType.Genre(name))
sealed class SearchType(val txt: String) {
class Text(txt: String) : SearchType(txt)
class Genre(txt: String) : SearchType(txt)
}
}

View File

@ -1,23 +1,23 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.map import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import com.bluelinelabs.conductor.Router
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.asState
import eu.kanade.core.prefs.mapAsCheckboxState import eu.kanade.core.prefs.mapAsCheckboxState
import eu.kanade.domain.UnsortedPreferences import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
@ -45,8 +45,7 @@ import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
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.tachiyomi.R
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.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@ -54,10 +53,8 @@ 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.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.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
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
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
@ -80,16 +77,21 @@ import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
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 eu.kanade.tachiyomi.util.system.toast
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import exh.source.getMainSource import exh.source.getMainSource
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
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.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -101,15 +103,15 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer import xyz.nulldev.ts.api.http.serializer.FilterSerializer
import java.util.Date import java.util.Date
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
open class BrowseSourcePresenter( open class BrowseSourceScreenModel(
private val sourceId: Long, private val sourceId: Long,
searchQuery: String? = null, searchQuery: String?,
// SY --> // SY -->
private val filtersJson: 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(),
preferences: BasePreferences = Injekt.get(), preferences: BasePreferences = Injekt.get(),
sourcePreferences: SourcePreferences = Injekt.get(), sourcePreferences: SourcePreferences = Injekt.get(),
@ -134,119 +136,35 @@ open class BrowseSourcePresenter(
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>(), BrowseSourceState by state { ) : StateScreenModel<BrowseSourceScreenModel.State>(State(Filter.valueOf(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 } }
var displayMode by sourcePreferences.sourceDisplayMode().asState() var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
val isIncognitoMode: Boolean by preferences.incognitoMode().asState() val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Sheet containing filter items.
*/
private var filterSheet: SourceFilterSheet? = null
// SY --> // SY -->
val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().asState() val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().asState(coroutineScope)
// SY <--
@Composable
fun getColumnsPreferenceForCurrentOrientation(): State<GridCells> {
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
return produceState<GridCells>(initialValue = GridCells.Adaptive(128.dp), isLandscape) {
(if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns())
.changes()
.collectLatest { columns ->
value = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
}
}
}
@Composable
fun getMangaList(): Flow<PagingData</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>> {
return remember(currentFilter) {
Pager(
PagingConfig(pageSize = 25),
) {
createSourcePagingSource(currentFilter.query, currentFilter.filters)
}.flow
.map {
it.map { (sManga, metadata) ->
// SY -->
withIOContext {
networkToLocalManga.await(sManga.toDomainManga(sourceId))
} to metadata
// SY <--
}
}
.cachedIn(presenterScope)
}
}
@Composable
fun getManga(initialManga: Manga): State<Manga> {
return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source)
.collectLatest { manga ->
if (manga == null) return@collectLatest
withIOContext {
initializeManga(manga)
}
value = manga
}
}
}
// SY -->
@Composable
open fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
return produceState(initialValue = initialMetadata, manga.id) {
val source = source?.getMainSource<MetadataSource<*, *>>() ?: return@produceState
getFlatMetadataById.subscribe(manga.id)
.collectLatest { metadata ->
if (metadata == null) return@collectLatest
value = metadata.raise(source.metaClass)
}
}
}
// SY <--
fun reset() {
val source = source ?: return
state.filters = source.getFilterList()
}
fun search(query: String? = null, filters: FilterList? = null) {
// SY -->
if (filters != null && filters !== state.filters) {
state.filters = filters
}
// SY <--
Filter.valueOf(query ?: "").let {
if (it !is Filter.UserInput) {
state.currentFilter = it
state.searchQuery = null
return
}
}
val input: Filter.UserInput = if (currentFilter is Filter.UserInput) currentFilter as Filter.UserInput else Filter.UserInput()
state.currentFilter = input.copy(
query = query ?: input.query,
filters = filters ?: input.filters,
)
}
// SY -->
private val filterSerializer = FilterSerializer() private val filterSerializer = FilterSerializer()
// SY <-- // SY <--
override fun onCreate(savedState: Bundle?) { init {
super.onCreate(savedState) mutableState.update { it.copy(filters = source.getFilterList()) }
state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
state.filters = source!!.getFilterList()
// SY --> // SY -->
val savedSearchFilters = savedSearch val savedSearchFilters = savedSearch
val jsonFilters = filtersJson val jsonFilters = filtersJson
val filters = state.value.filters
if (savedSearchFilters != null) { if (savedSearchFilters != null) {
val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { filters } } val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { filters } }
if (savedSearch != null) { if (savedSearch != null) {
@ -254,22 +172,140 @@ open class BrowseSourcePresenter(
} }
} else if (jsonFilters != null) { } else if (jsonFilters != null) {
runCatching { runCatching {
val filters = Json.decodeFromString<JsonArray>(jsonFilters) val filtersJson = Json.decodeFromString<JsonArray>(jsonFilters)
filterSerializer.deserialize(this.filters, filters) filterSerializer.deserialize(filters, filtersJson)
search(filters = this.filters) search(filters = filters)
} }
} }
getExhSavedSearch.subscribe(source!!.id, source!!::getFilterList) getExhSavedSearch.subscribe(source.id, source::getFilterList)
.onEach { .onEach {
withUIContext { withUIContext {
view?.setSavedSearches(it) filterSheet?.setSavedSearches(it)
} }
} }
.launchIn(presenterScope) .launchIn(coroutineScope)
// SY <-- // SY <--
} }
fun getColumnsPreference(orientation: Int): GridCells {
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
val columns = if (isLandscape) {
libraryPreferences.landscapeColumns()
} else {
libraryPreferences.portraitColumns()
}.get()
return if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
}
fun getMangaListFlow(currentFilter: Filter): Flow<PagingData<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>> {
return Pager(
PagingConfig(pageSize = 25),
) {
// SY -->
createSourcePagingSource(currentFilter.query ?: "", currentFilter.filters)
// SY <--
}.flow
.map { pagingData ->
pagingData.map { (sManga, metadata) ->
val dbManga = withIOContext { networkToLocalManga.await(sManga.toDomainManga(sourceId)) }
getManga.subscribe(dbManga.url, dbManga.source)
.filterNotNull()
.onEach { initializeManga(it) }
// SY -->
.combineMetadata(dbManga, metadata)
// SY <--
.stateIn(coroutineScope)
}
}
.cachedIn(coroutineScope)
}
// SY -->
open fun Flow<Manga>.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
val metadataSource = source.getMainSource<MetadataSource<*, *>>()
return combine(getFlatMetadataById.subscribe(dbManga.id)) { manga, flatMetadata ->
metadataSource ?: return@combine manga to metadata
manga to (flatMetadata?.raise(metadataSource.metaClass) ?: metadata)
}
}
// SY <--
fun reset() {
mutableState.update { it.copy(filters = source.getFilterList()) }
}
fun search(query: String? = null, filters: FilterList? = null) {
// SY -->
if (filters != null && filters !== state.value.filters) {
mutableState.update { state -> state.copy(filters = filters) }
}
// SY <--
Filter.valueOf(query).let {
if (it !is Filter.UserInput) {
mutableState.update { state -> state.copy(currentFilter = it) }
return
}
}
val input = if (state.value.currentFilter is Filter.UserInput) {
state.value.currentFilter as Filter.UserInput
} else {
Filter.UserInput()
}
mutableState.update {
it.copy(
currentFilter = input.copy(
query = query ?: input.query,
filters = filters ?: input.filters,
),
toolbarQuery = query ?: input.query,
)
}
}
fun searchGenre(genreName: String) {
val defaultFilters = source.getFilterList()
var genreExists = false
filter@ for (sourceFilter in defaultFilters) {
if (sourceFilter is SourceModelFilter.Group<*>) {
for (filter in sourceFilter.state) {
if (filter is SourceModelFilter<*> && filter.name.equals(genreName, true)) {
when (filter) {
is SourceModelFilter.TriState -> filter.state = 1
is SourceModelFilter.CheckBox -> filter.state = true
else -> {}
}
genreExists = true
break@filter
}
}
} else if (sourceFilter is SourceModelFilter.Select<*>) {
val index = sourceFilter.values.filterIsInstance<String>()
.indexOfFirst { it.equals(genreName, true) }
if (index != -1) {
sourceFilter.state = index
genreExists = true
break
}
}
}
mutableState.update {
val filter = if (genreExists) {
Filter.UserInput(filters = defaultFilters)
} else {
Filter.UserInput(query = genreName)
}
it.copy(
filters = defaultFilters,
currentFilter = filter,
)
}
}
/** /**
* Initialize a manga. * Initialize a manga.
* *
@ -279,7 +315,7 @@ open class BrowseSourcePresenter(
if (manga.thumbnailUrl != null || manga.initialized) return if (manga.thumbnailUrl != null || manga.initialized) return
withNonCancellableContext { withNonCancellableContext {
try { try {
val networkManga = source!!.getMangaDetails(manga.toSManga()) val networkManga = source.getMangaDetails(manga.toSManga())
val updatedManga = manga.copyFrom(networkManga) val updatedManga = manga.copyFrom(networkManga)
.copy(initialized = true) .copy(initialized = true)
@ -296,7 +332,7 @@ open class BrowseSourcePresenter(
* @param manga the manga to update. * @param manga the manga to update.
*/ */
fun changeMangaFavorite(manga: Manga) { fun changeMangaFavorite(manga: Manga) {
presenterScope.launch { coroutineScope.launch {
var new = manga.copy( var new = manga.copy(
favorite = !manga.favorite, favorite = !manga.favorite,
dateAdded = when (manga.favorite) { dateAdded = when (manga.favorite) {
@ -322,7 +358,7 @@ open class BrowseSourcePresenter(
} }
fun addFavorite(manga: Manga) { fun addFavorite(manga: Manga) {
presenterScope.launch { coroutineScope.launch {
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -345,7 +381,7 @@ open class BrowseSourcePresenter(
// Choose a category // Choose a category
else -> { else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id } val preselectedIds = getCategories.await(manga.id).map { it.id }
state.dialog = Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }) setDialog(Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }))
} }
} }
} }
@ -354,7 +390,7 @@ open class BrowseSourcePresenter(
private suspend fun autoAddTrack(manga: Manga) { private suspend fun autoAddTrack(manga: Manga) {
loggedServices loggedServices
.filterIsInstance<EnhancedTrackService>() .filterIsInstance<EnhancedTrackService>()
.filter { it.accept(source!!) } .filter { it.accept(source) }
.forEach { service -> .forEach { service ->
try { try {
service.match(manga.toDbManga())?.let { track -> service.match(manga.toDbManga())?.let { track ->
@ -373,7 +409,7 @@ open class BrowseSourcePresenter(
// SY --> // SY -->
open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType { open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return getRemoteManga.subscribe(sourceId, currentFilter.query, currentFilter.filters) return getRemoteManga.subscribe(sourceId, query, filters)
} }
// SY <-- // SY <--
@ -398,7 +434,7 @@ open class BrowseSourcePresenter(
} }
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) { fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
presenterScope.launchIO { coroutineScope.launchIO {
setMangaCategories.await( setMangaCategories.await(
mangaId = manga.id, mangaId = manga.id,
categoryIds = categoryIds.toList(), categoryIds = categoryIds.toList(),
@ -406,13 +442,82 @@ open class BrowseSourcePresenter(
} }
} }
sealed class Filter(open val query: String, open val filters: FilterList) { fun openFilterSheet() {
filterSheet?.show()
}
fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) }
}
fun setToolbarQuery(query: String?) {
mutableState.update { it.copy(toolbarQuery = query) }
}
open fun initFilterSheet(context: Context, router: Router) {
val state = state.value
/*if (state.filters.isEmpty()) {
return
}*/
filterSheet = SourceFilterSheet(
context = context,
// SY -->
router = router,
source = source,
searches = emptyList(),
// SY <--
onFilterClicked = { search(filters = state.filters) },
onResetClicked = {
reset()
filterSheet?.setFilters(state.filterItems)
},
// EXH -->
onSaveClicked = {
coroutineScope.launchIO {
val names = loadSearches().map { it.name }
mutableState.update { it.copy(dialog = Dialog.CreateSavedSearh(names)) }
}
},
onSavedSearchClicked = { idOfSearch ->
coroutineScope.launchIO {
val search = loadSearch(idOfSearch)
if (search == null) {
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
return@launchIO
}
if (search.filterList == null && state.filters.isNotEmpty()) {
context.toast(R.string.save_search_invalid)
return@launchIO
}
val allDefault = search.filterList != null && state.filters == source.getFilterList()
filterSheet?.dismiss()
search(
query = search.query,
filters = if (allDefault) null else search.filterList,
)
}
},
onSavedSearchDeleteClicked = { idToDelete, name ->
mutableState.update { it.copy(dialog = Dialog.DeleteSavedSearch(idToDelete, name)) }
},
// EXH <--
)
filterSheet?.setFilters(state.filterItems)
}
sealed class Filter(open val query: String?, open val filters: FilterList) {
object Popular : Filter(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList()) object Popular : Filter(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList())
object Latest : Filter(query = GetRemoteManga.QUERY_LATEST, filters = FilterList()) object Latest : Filter(query = GetRemoteManga.QUERY_LATEST, filters = FilterList())
data class UserInput(override val query: String = "", override val filters: FilterList = FilterList()) : Filter(query = query, filters = filters) data class UserInput(override val query: String? = null, override val filters: FilterList = FilterList()) : Filter(query = query, filters = filters)
companion object { companion object {
fun valueOf(query: String): Filter { fun valueOf(query: String?): Filter {
return when (query) { return when (query) {
GetRemoteManga.QUERY_POPULAR -> Popular GetRemoteManga.QUERY_POPULAR -> Popular
GetRemoteManga.QUERY_LATEST -> Latest GetRemoteManga.QUERY_LATEST -> Latest
@ -430,17 +535,42 @@ open class BrowseSourcePresenter(
val initialSelection: List<CheckboxState.State<Category>>, val initialSelection: List<CheckboxState.State<Category>>,
) : Dialog() ) : Dialog()
data class Migrate(val newManga: Manga) : Dialog() data class Migrate(val newManga: Manga) : Dialog()
// SY -->
object FailedToLoadSavedSearch : Dialog()
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog()
data class CreateSavedSearh(val currentSavedSearches: List<String>) : Dialog()
// SY <--
}
@Immutable
data class State(
val currentFilter: Filter,
val filters: FilterList = FilterList(),
val toolbarQuery: String? = null,
val dialog: Dialog? = null,
) {
val filterItems = filters.toItems()
val isUserQuery = currentFilter is Filter.UserInput && !currentFilter.query.isNullOrEmpty()
val searchQuery = when (currentFilter) {
is Filter.UserInput -> currentFilter.query
Filter.Latest, Filter.Popular -> null
}
} }
// EXH --> // EXH -->
fun saveSearch(name: String, query: String, filterList: FilterList) { fun saveSearch(
presenterScope.launchNonCancellable { name: String,
) {
coroutineScope.launchNonCancellable {
val query = state.value.currentFilter.query
val filterList = state.value.currentFilter.filters.ifEmpty { source.getFilterList() }
insertSavedSearch.await( insertSavedSearch.await(
SavedSearch( SavedSearch(
id = -1, id = -1,
source = source!!.id, source = source.id,
name = name.trim(), name = name.trim(),
query = query.nullIfBlank(), query = query?.nullIfBlank(),
filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(), filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(),
), ),
) )
@ -448,41 +578,41 @@ open class BrowseSourcePresenter(
} }
fun deleteSearch(savedSearchId: Long) { fun deleteSearch(savedSearchId: Long) {
presenterScope.launchNonCancellable { coroutineScope.launchNonCancellable {
deleteSavedSearchById.await(savedSearchId) deleteSavedSearchById.await(savedSearchId)
} }
} }
suspend fun loadSearch(searchId: Long) = suspend fun loadSearch(searchId: Long) =
getExhSavedSearch.awaitOne(searchId, source!!::getFilterList) getExhSavedSearch.awaitOne(searchId, source::getFilterList)
suspend fun loadSearches() = suspend fun loadSearches() =
getExhSavedSearch.await(source!!.id, source!!::getFilterList) getExhSavedSearch.await(source.id, source::getFilterList)
// EXH <-- // EXH <--
} }
fun FilterList.toItems(): List<IFlexible<*>> { fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull { filter -> return mapNotNull { filter ->
when (filter) { when (filter) {
is Filter.Header -> HeaderItem(filter)
// --> EXH // --> EXH
is Filter.AutoComplete -> AutoComplete(filter) is SourceModelFilter.AutoComplete -> AutoComplete(filter)
// <-- EXH // <-- EXH
is Filter.Separator -> SeparatorItem(filter) is SourceModelFilter.Header -> HeaderItem(filter)
is Filter.CheckBox -> CheckboxItem(filter) is SourceModelFilter.Separator -> SeparatorItem(filter)
is Filter.TriState -> TriStateItem(filter) is SourceModelFilter.CheckBox -> CheckboxItem(filter)
is Filter.Text -> TextItem(filter) is SourceModelFilter.TriState -> TriStateItem(filter)
is Filter.Select<*> -> SelectItem(filter) is SourceModelFilter.Text -> TextItem(filter)
is Filter.Group<*> -> { is SourceModelFilter.Select<*> -> SelectItem(filter)
is SourceModelFilter.Group<*> -> {
val group = GroupItem(filter) val group = GroupItem(filter)
val subItems = filter.state.mapNotNull { val subItems = filter.state.mapNotNull {
when (it) { when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it) is SourceModelFilter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it) is SourceModelFilter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it) is SourceModelFilter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it) is SourceModelFilter.Select<*> -> SelectSectionItem(it)
// SY --> // SY -->
is Filter.AutoComplete -> AutoCompleteSectionItem(it) is SourceModelFilter.AutoComplete -> AutoCompleteSectionItem(it)
// SY <-- // SY <--
else -> null else -> null
} }
@ -491,7 +621,7 @@ fun FilterList.toItems(): List<IFlexible<*>> {
group.subItems = subItems group.subItems = subItems
group group
} }
is Filter.Sort -> { is SourceModelFilter.Sort -> {
val group = SortGroup(filter) val group = SortGroup(filter)
val subItems = filter.values.map { val subItems = filter.values.map {
SortItem(it, group) SortItem(it, group)

View File

@ -17,9 +17,9 @@ import com.bluelinelabs.conductor.Router
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.interactor.GetRemoteManga import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.SourceFeedScreen import eu.kanade.presentation.browse.SourceFeedScreen
import eu.kanade.presentation.browse.components.FailedToLoadSavedSearchDialog
import eu.kanade.presentation.browse.components.SourceFeedAddDialog import eu.kanade.presentation.browse.components.SourceFeedAddDialog
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
import eu.kanade.presentation.browse.components.SourceFeedFailedToLoadSavedSearchDialog
import eu.kanade.presentation.util.LocalRouter import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@ -89,7 +89,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
) )
} }
SourceFeedScreenModel.Dialog.FailedToLoadSavedSearch -> { SourceFeedScreenModel.Dialog.FailedToLoadSavedSearch -> {
SourceFeedFailedToLoadSavedSearchDialog(onDismissRequest) FailedToLoadSavedSearchDialog(onDismissRequest)
} }
null -> Unit null -> Unit
} }

View File

@ -44,7 +44,7 @@ class GlobalSearchScreen(
if (!screenModel.incognitoMode.get()) { if (!screenModel.incognitoMode.get()) {
screenModel.lastUsedSourceId.set(it.id) screenModel.lastUsedSourceId.set(it.id)
} }
router.pushController(BrowseSourceController(it, state.searchQuery)) router.pushController(BrowseSourceController(it.id, state.searchQuery))
}, },
onClickItem = { router.pushController(MangaController(it.id, true)) }, onClickItem = { router.pushController(MangaController(it.id, true)) },
onLongClickItem = { router.pushController(MangaController(it.id, true)) }, onLongClickItem = { router.pushController(MangaController(it.id, true)) },

View File

@ -2,24 +2,16 @@ package exh.md.follows
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import eu.kanade.presentation.browse.BrowseMangadexFollowsScreen import cafe.adriel.voyager.navigator.Navigator
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.base.controller.BasicFullComposeController
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.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].
*/ */
class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle) { class MangaDexFollowsController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(source: CatalogueSource) : this( constructor(source: CatalogueSource) : this(
bundleOf( bundleOf(
@ -27,71 +19,12 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle)
), ),
) )
override fun createPresenter(): BrowseSourcePresenter { private val sourceId = args.getLong(SOURCE_ID_KEY)
return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY))
}
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
val scope = rememberCoroutineScope() Navigator(screen = MangaDexFollowsScreen(sourceId))
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)
},
mangaToRemove = 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)
},
)
}
is BrowseSourcePresenter.Dialog.Migrate -> Unit
null -> {}
}
}
override fun initFilterSheet() {
// No-op: we don't allow filtering in mangadex follows
} }
} }
private const val SOURCE_ID_KEY = "source_id"

View File

@ -1,28 +0,0 @@
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 eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
/**
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
*/
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return MangaDexFollowsPagingSource(source!!.getMainSource() as MangaDex)
}
@Composable
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
return remember { mutableStateOf(initialMetadata) }
}
}

View File

@ -0,0 +1,143 @@
package exh.md.follows
import androidx.activity.compose.BackHandler
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.DuplicateMangaDialog
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO
class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
@Composable
override fun Content() {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
val screenModel = rememberScreenModel { MangaDexFollowsScreenModel(sourceId) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
title = stringResource(R.string.mangadex_follows),
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
// SY -->
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
// SY <--
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = null,
onHelpClick = null,
onLocalSourceHelpClick = null,
onMangaClick = { router.pushController(MangaController(it.id, true)) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
)
else -> screenModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
},
)
}
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
duplicateFrom = screenModel.getSourceOrStub(dialog.duplicate),
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = {
router.pushController(CategoryController())
},
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
},
)
}
else -> {}
}
BackHandler(onBack = navigateUp)
}
}

View File

@ -0,0 +1,28 @@
package exh.md.follows
import android.content.Context
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) {
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return MangaDexFollowsPagingSource(source.getMainSource() as MangaDex)
}
override fun Flow<Manga>.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
return map { it to metadata }
}
override fun initFilterSheet(context: Context, router: Router) {
// No-op: we don't allow filtering in recs
}
}

View File

@ -2,54 +2,33 @@ package exh.md.similar
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
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.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
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.manga.MangaController
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/ */
class MangaDexSimilarController(bundle: Bundle) : BrowseSourceController(bundle) { class MangaDexSimilarController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(manga: Manga, source: CatalogueSource) : this( constructor(manga: Manga, source: CatalogueSource) : this(
bundleOf( bundleOf(
MANGA_ID to manga.id, MANGA_ID to manga.id,
MANGA_TITLE to manga.title,
SOURCE_ID_KEY to source.id, SOURCE_ID_KEY to source.id,
), ),
) )
private val mangaTitle = args.getString(MANGA_TITLE, "") val mangaId = args.getLong(MANGA_ID)
val sourceId = args.getLong(SOURCE_ID_KEY)
override fun createPresenter(): BrowseSourcePresenter {
return MangaDexSimilarPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY))
}
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
BrowseRecommendationsScreen( Navigator(screen = MangaDexSimilarScreen(mangaId, sourceId))
presenter = presenter,
navigateUp = { router.popCurrentController() },
title = stringResource(R.string.similar, mangaTitle),
onMangaClick = {
router.pushController(MangaController(it.id, true))
},
)
}
override fun initFilterSheet() {
// No-op: we don't allow filtering in similar
}
companion object {
const val MANGA_ID = "manga_id"
const val MANGA_TITLE = "manga_title"
} }
} }
private const val MANGA_ID = "manga_id"
private const val SOURCE_ID_KEY = "source_id"

View File

@ -1,44 +0,0 @@
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 eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter.
*/
class MangaDexSimilarPresenter(
val mangaId: Long,
sourceId: Long,
private val getManga: GetManga = Injekt.get(),
) : BrowseSourcePresenter(sourceId) {
var manga: Manga? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
this.manga = runBlocking { getManga.await(mangaId) }
}
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return MangaDexSimilarPagingSource(manga!!, source!!.getMainSource() as MangaDex)
}
@Composable
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
return remember { mutableStateOf(initialMetadata) }
}
}

View File

@ -0,0 +1,81 @@
package exh.md.similar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.manga.MangaController
class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { MangaDexSimilarScreenModel(mangaId, sourceId) }
val state by screenModel.state.collectAsState()
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val onMangaClick: (Manga) -> Unit = {
router.pushController(MangaController(it.id, true))
}
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
navigateUp = navigateUp,
title = stringResource(R.string.similar, screenModel.manga.title),
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
scrollBehavior = scrollBehavior,
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
// SY -->
ehentaiBrowseDisplayMode = false,
// SY <--
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = null,
onHelpClick = null,
onLocalSourceHelpClick = null,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaClick,
)
}
}
}

View File

@ -0,0 +1,41 @@
package exh.md.similar
import android.content.Context
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter.
*/
class MangaDexSimilarScreenModel(
val mangaId: Long,
sourceId: Long,
private val getManga: GetManga = Injekt.get(),
) : BrowseSourceScreenModel(sourceId, null) {
val manga: Manga = runBlocking { getManga.await(mangaId) }!!
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return MangaDexSimilarPagingSource(manga, source.getMainSource() as MangaDex)
}
override fun Flow<Manga>.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
return map { it to metadata }
}
override fun initFilterSheet(context: Context, router: Router) {
// No-op: we don't allow filtering in recs
}
}

View File

@ -3,17 +3,16 @@ package exh.recs
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
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.BasicFullComposeController
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
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/ */
class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) { class RecommendsController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(manga: Manga, source: CatalogueSource) : this( constructor(manga: Manga, source: CatalogueSource) : this(
bundleOf( bundleOf(
@ -22,38 +21,14 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
), ),
) )
override fun createPresenter(): RecommendsPresenter { val mangaId = args.getLong(MANGA_ID)
return RecommendsPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY)) val sourceId = args.getLong(SOURCE_ID_KEY)
}
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
BrowseRecommendationsScreen( Navigator(screen = RecommendsScreen(mangaId, sourceId))
presenter = presenter,
navigateUp = { router.popCurrentController() },
title = (presenter as RecommendsPresenter).manga!!.title,
onMangaClick = { manga ->
openSmartSearch(manga.ogTitle)
},
)
}
override fun initFilterSheet() {
// No-op: we don't allow filtering in recs
}
private fun openSmartSearch(title: String) {
val smartSearchConfig = SourcesController.SmartSearchConfig(title)
router.pushController(
SourcesController(
bundleOf(
SourcesController.SMART_SEARCH_CONFIG to smartSearchConfig,
),
),
)
}
companion object {
const val MANGA_ID = "manga_id"
} }
} }
private const val MANGA_ID = "manga_id"
private const val SOURCE_ID_KEY = "source_id"

View File

@ -0,0 +1,92 @@
package exh.recs
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.core.os.bundleOf
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { RecommendsScreenModel(mangaId, sourceId) }
val state by screenModel.state.collectAsState()
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val onMangaClick: (Manga) -> Unit = { manga ->
openSmartSearch(router, manga.ogTitle)
}
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
navigateUp = navigateUp,
title = screenModel.manga.title,
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
scrollBehavior = scrollBehavior,
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
// SY -->
ehentaiBrowseDisplayMode = false,
// SY <--
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = null,
onHelpClick = null,
onLocalSourceHelpClick = null,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaClick,
)
}
}
private fun openSmartSearch(router: Router, title: String) {
val smartSearchConfig = SourcesController.SmartSearchConfig(title)
router.pushController(
SourcesController(
bundleOf(
SourcesController.SMART_SEARCH_CONFIG to smartSearchConfig,
),
),
)
}
}

View File

@ -1,11 +1,9 @@
package exh.recs package exh.recs
import android.os.Bundle
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.source.model.SourcePagingSourceType import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
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
@ -13,20 +11,15 @@ import uy.kohesive.injekt.api.get
/** /**
* Presenter of [RecommendsController]. Inherit BrowseCataloguePresenter. * Presenter of [RecommendsController]. Inherit BrowseCataloguePresenter.
*/ */
class RecommendsPresenter( class RecommendsScreenModel(
val mangaId: Long, val mangaId: Long,
sourceId: Long, sourceId: Long,
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
) : BrowseSourcePresenter(sourceId) { ) : BrowseSourceScreenModel(sourceId, null) {
var manga: Manga? = null val manga = runBlocking { getManga.await(mangaId) }!!
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
this.manga = runBlocking { getManga.await(mangaId) }
}
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType { override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return RecommendsPagingSource(source!!, manga!!) return RecommendsPagingSource(source, manga)
} }
} }