Use Voyager for source feed
This commit is contained in:
parent
bd73eff732
commit
658c84bef8
@ -34,7 +34,6 @@ import eu.kanade.presentation.components.SearchToolbar
|
|||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedPresenter
|
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
import exh.savedsearches.models.SavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
|
||||||
@ -93,41 +92,49 @@ sealed class SourceFeedUI {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceFeedScreen(
|
fun SourceFeedScreen(
|
||||||
presenter: SourceFeedPresenter,
|
name: String,
|
||||||
onFabClick: () -> Unit,
|
isLoading: Boolean,
|
||||||
|
items: List<SourceFeedUI>,
|
||||||
|
onFabClick: (() -> Unit)?,
|
||||||
onClickBrowse: () -> Unit,
|
onClickBrowse: () -> Unit,
|
||||||
onClickLatest: () -> Unit,
|
onClickLatest: () -> Unit,
|
||||||
onClickSavedSearch: (SavedSearch) -> Unit,
|
onClickSavedSearch: (SavedSearch) -> Unit,
|
||||||
onClickDelete: (FeedSavedSearch) -> Unit,
|
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||||
onClickManga: (Manga) -> Unit,
|
onClickManga: (Manga) -> Unit,
|
||||||
onClickSearch: (String) -> Unit,
|
onClickSearch: (String) -> Unit,
|
||||||
|
searchQuery: String?,
|
||||||
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
|
isIncognitoMode: Boolean,
|
||||||
|
isDownloadOnly: Boolean,
|
||||||
|
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
SourceFeedToolbar(
|
SourceFeedToolbar(
|
||||||
title = presenter.source.name,
|
title = name,
|
||||||
state = presenter,
|
searchQuery = searchQuery,
|
||||||
|
onSearchQueryChange = onSearchQueryChange,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
incognitoMode = presenter.isIncognitoMode,
|
incognitoMode = isIncognitoMode,
|
||||||
downloadedOnlyMode = presenter.isDownloadOnly,
|
downloadedOnlyMode = isDownloadOnly,
|
||||||
onClickSearch = onClickSearch,
|
onClickSearch = onClickSearch,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
BrowseSourceFloatingActionButton(
|
BrowseSourceFloatingActionButton(
|
||||||
isVisible = presenter.filterItems.isNotEmpty(),
|
isVisible = onFabClick != null,
|
||||||
onFabClick = onFabClick,
|
onFabClick = onFabClick ?: {},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Crossfade(targetState = presenter.isLoading) { state ->
|
Crossfade(targetState = isLoading) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
true -> LoadingScreen()
|
true -> LoadingScreen()
|
||||||
false -> {
|
false -> {
|
||||||
SourceFeedList(
|
SourceFeedList(
|
||||||
state = presenter,
|
items = items,
|
||||||
paddingValues = paddingValues,
|
paddingValues = paddingValues,
|
||||||
getMangaState = { presenter.getManga(it) },
|
getMangaState = getMangaState,
|
||||||
onClickBrowse = onClickBrowse,
|
onClickBrowse = onClickBrowse,
|
||||||
onClickLatest = onClickLatest,
|
onClickLatest = onClickLatest,
|
||||||
onClickSavedSearch = onClickSavedSearch,
|
onClickSavedSearch = onClickSavedSearch,
|
||||||
@ -142,7 +149,7 @@ fun SourceFeedScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceFeedList(
|
fun SourceFeedList(
|
||||||
state: SourceFeedState,
|
items: List<SourceFeedUI>,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||||
onClickBrowse: () -> Unit,
|
onClickBrowse: () -> Unit,
|
||||||
@ -155,7 +162,7 @@ fun SourceFeedList(
|
|||||||
contentPadding = paddingValues + topSmallPaddingValues,
|
contentPadding = paddingValues + topSmallPaddingValues,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
state.items.orEmpty(),
|
items.orEmpty(),
|
||||||
key = { it.id },
|
key = { it.id },
|
||||||
) { item ->
|
) { item ->
|
||||||
SourceFeedItem(
|
SourceFeedItem(
|
||||||
@ -248,7 +255,8 @@ fun SourceFeedItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SourceFeedToolbar(
|
fun SourceFeedToolbar(
|
||||||
title: String,
|
title: String,
|
||||||
state: SourceFeedState,
|
searchQuery: String?,
|
||||||
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
incognitoMode: Boolean,
|
incognitoMode: Boolean,
|
||||||
downloadedOnlyMode: Boolean,
|
downloadedOnlyMode: Boolean,
|
||||||
@ -256,10 +264,10 @@ fun SourceFeedToolbar(
|
|||||||
) {
|
) {
|
||||||
SearchToolbar(
|
SearchToolbar(
|
||||||
titleContent = { AppBarTitle(title) },
|
titleContent = { AppBarTitle(title) },
|
||||||
searchQuery = state.searchQuery,
|
searchQuery = searchQuery,
|
||||||
onChangeSearchQuery = { state.searchQuery = it },
|
onChangeSearchQuery = onSearchQueryChange,
|
||||||
onSearch = onClickSearch,
|
onSearch = onClickSearch,
|
||||||
onClickCloseSearch = { state.searchQuery = null },
|
onClickCloseSearch = { onSearchQueryChange(null) },
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
incognitoMode = incognitoMode,
|
incognitoMode = incognitoMode,
|
||||||
downloadedOnlyMode = downloadedOnlyMode,
|
downloadedOnlyMode = downloadedOnlyMode,
|
||||||
|
@ -1,31 +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.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
interface SourceFeedState {
|
|
||||||
val isLoading: Boolean
|
|
||||||
var searchQuery: String?
|
|
||||||
val filters: FilterList
|
|
||||||
val filterItems: List<IFlexible<*>>
|
|
||||||
val items: List<SourceFeedUI>?
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SourceFeedState(): SourceFeedState {
|
|
||||||
return SourceFeedStateImpl()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SourceFeedStateImpl : SourceFeedState {
|
|
||||||
override var isLoading: Boolean by mutableStateOf(true)
|
|
||||||
override var searchQuery: String? by mutableStateOf(null)
|
|
||||||
override var filters: FilterList by mutableStateOf(FilterList())
|
|
||||||
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
|
|
||||||
override var items: List<SourceFeedUI>? by mutableStateOf(null)
|
|
||||||
}
|
|
@ -0,0 +1,81 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceFeedAddDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
name: String,
|
||||||
|
addFeed: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = addFeed) {
|
||||||
|
Text(text = stringResource(R.string.action_add))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.feed))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.feed_add, name))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceFeedDeleteDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
deleteFeed: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = deleteFeed) {
|
||||||
|
Text(text = stringResource(R.string.action_delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.feed))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.feed_delete))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -208,7 +208,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
filterSheet = SourceFilterSheet(
|
filterSheet = SourceFilterSheet(
|
||||||
activity!!,
|
activity!!,
|
||||||
// SY -->
|
// SY -->
|
||||||
this,
|
router,
|
||||||
presenter.source!!,
|
presenter.source!!,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
// SY <--
|
// SY <--
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -8,13 +7,13 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import com.bluelinelabs.conductor.Router
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
|
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
|
||||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
|
import eu.kanade.tachiyomi.widget.SimpleNavigationView
|
||||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||||
import exh.md.MangaDexFabHeaderAdapter
|
import exh.md.MangaDexFabHeaderAdapter
|
||||||
@ -22,9 +21,9 @@ import exh.savedsearches.EXHSavedSearch
|
|||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
|
|
||||||
class SourceFilterSheet(
|
class SourceFilterSheet(
|
||||||
activity: Activity,
|
context: Context,
|
||||||
// SY -->
|
// SY -->
|
||||||
controller: BaseController<*>,
|
router: Router,
|
||||||
source: CatalogueSource,
|
source: CatalogueSource,
|
||||||
searches: List<EXHSavedSearch> = emptyList(),
|
searches: List<EXHSavedSearch> = emptyList(),
|
||||||
// SY <--
|
// SY <--
|
||||||
@ -35,14 +34,14 @@ class SourceFilterSheet(
|
|||||||
var onSavedSearchClicked: (Long) -> Unit = {},
|
var onSavedSearchClicked: (Long) -> Unit = {},
|
||||||
var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> },
|
var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> },
|
||||||
// EXH <--
|
// EXH <--
|
||||||
) : BaseBottomSheetDialog(activity) {
|
) : BaseBottomSheetDialog(context) {
|
||||||
|
|
||||||
private var filterNavView: FilterNavigationView = FilterNavigationView(
|
private var filterNavView: FilterNavigationView = FilterNavigationView(
|
||||||
activity,
|
context,
|
||||||
// SY -->
|
// SY -->
|
||||||
searches = searches,
|
searches = searches,
|
||||||
source = source,
|
source = source,
|
||||||
controller = controller,
|
router = router,
|
||||||
dismissSheet = ::dismiss,
|
dismissSheet = ::dismiss,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
@ -85,7 +84,7 @@ class SourceFilterSheet(
|
|||||||
// SY -->
|
// SY -->
|
||||||
searches: List<EXHSavedSearch> = emptyList(),
|
searches: List<EXHSavedSearch> = emptyList(),
|
||||||
source: CatalogueSource? = null,
|
source: CatalogueSource? = null,
|
||||||
controller: BaseController<*>? = null,
|
router: Router? = null,
|
||||||
dismissSheet: (() -> Unit)? = null,
|
dismissSheet: (() -> Unit)? = null,
|
||||||
// SY <--
|
// SY <--
|
||||||
) :
|
) :
|
||||||
@ -117,10 +116,10 @@ class SourceFilterSheet(
|
|||||||
// SY -->
|
// SY -->
|
||||||
recycler.adapter = ConcatAdapter(
|
recycler.adapter = ConcatAdapter(
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
controller?.let {
|
router?.let {
|
||||||
source?.getMainSource<MangaDex>()
|
source?.getMainSource<MangaDex>()
|
||||||
?.let {
|
?.let {
|
||||||
MangaDexFabHeaderAdapter(controller, it) {
|
MangaDexFabHeaderAdapter(router, it) {
|
||||||
dismissSheet?.invoke()
|
dismissSheet?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,229 +1,34 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
|
||||||
import eu.kanade.presentation.browse.SourceFeedScreen
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
|
||||||
import exh.savedsearches.models.SavedSearch
|
|
||||||
import exh.util.nullIfBlank
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
|
||||||
|
|
||||||
/**
|
class SourceFeedController : BasicFullComposeController {
|
||||||
* This controller shows and manages the different search result in global search.
|
|
||||||
* This controller should only handle UI actions, IO actions should be done by [SourceFeedPresenter]
|
|
||||||
* [SourceFeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
|
||||||
*/
|
|
||||||
open class SourceFeedController :
|
|
||||||
FullComposeController<SourceFeedPresenter> {
|
|
||||||
|
|
||||||
constructor(source: CatalogueSource?) : super(
|
constructor(source: CatalogueSource) : super(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
SOURCE_EXTRA to (source?.id ?: 0),
|
SOURCE_EXTRA to source.id,
|
||||||
),
|
),
|
||||||
) {
|
)
|
||||||
this.source = source
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(sourceId: Long) : this(
|
constructor(sourceId: Long) : super(
|
||||||
Injekt.get<SourceManager>().get(sourceId) as? CatalogueSource,
|
bundleOf(
|
||||||
|
SOURCE_EXTRA to sourceId,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor(bundle: Bundle) : this(bundle.getLong(SOURCE_EXTRA))
|
constructor(bundle: Bundle) : super(bundle)
|
||||||
|
|
||||||
var source: CatalogueSource? = null
|
val sourceId = args.getLong(SOURCE_EXTRA)
|
||||||
|
|
||||||
/**
|
|
||||||
* Sheet containing filter items.
|
|
||||||
*/
|
|
||||||
private var filterSheet: SourceFilterSheet? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the [SourceFeedPresenter] used in controller.
|
|
||||||
*
|
|
||||||
* @return instance of [SourceFeedPresenter]
|
|
||||||
*/
|
|
||||||
override fun createPresenter(): SourceFeedPresenter {
|
|
||||||
return SourceFeedPresenter(source = source!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the view is created
|
|
||||||
*
|
|
||||||
* @param view view of controller
|
|
||||||
*/
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
|
|
||||||
// Prepare filter sheet
|
|
||||||
initFilterSheet()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val filterSerializer = FilterSerializer()
|
|
||||||
|
|
||||||
fun initFilterSheet() {
|
|
||||||
filterSheet = SourceFilterSheet(
|
|
||||||
activity!!,
|
|
||||||
// SY -->
|
|
||||||
this,
|
|
||||||
presenter.source,
|
|
||||||
emptyList(),
|
|
||||||
// SY <--
|
|
||||||
onFilterClicked = {
|
|
||||||
val allDefault = presenter.filters == presenter.source.getFilterList()
|
|
||||||
filterSheet?.dismiss()
|
|
||||||
if (allDefault) {
|
|
||||||
onBrowseClick(
|
|
||||||
presenter.searchQuery?.nullIfBlank(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
onBrowseClick(
|
|
||||||
presenter.searchQuery?.nullIfBlank(),
|
|
||||||
filters = Json.encodeToString(filterSerializer.serialize(presenter.filters)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onResetClicked = {},
|
|
||||||
onSaveClicked = {},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search.filterList != null) {
|
|
||||||
presenter.setFilters(FilterList(search.filterList))
|
|
||||||
filterSheet?.setFilters(presenter.filterItems)
|
|
||||||
}
|
|
||||||
val allDefault = search.filterList != null && presenter.filters == presenter.source.getFilterList()
|
|
||||||
filterSheet?.dismiss()
|
|
||||||
|
|
||||||
if (!allDefault) {
|
|
||||||
onBrowseClick(
|
|
||||||
search = presenter.searchQuery?.nullIfBlank(),
|
|
||||||
savedSearch = search.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSavedSearchDeleteClicked = { idOfSearch, name ->
|
|
||||||
viewScope.launchUI {
|
|
||||||
if (presenter.hasTooManyFeeds()) {
|
|
||||||
activity?.toast(R.string.too_many_in_feed)
|
|
||||||
return@launchUI
|
|
||||||
}
|
|
||||||
withUIContext {
|
|
||||||
MaterialAlertDialogBuilder(activity!!)
|
|
||||||
.setTitle(R.string.feed)
|
|
||||||
.setMessage(activity!!.getString(R.string.feed_add, name))
|
|
||||||
.setPositiveButton(R.string.action_add) { _, _ ->
|
|
||||||
presenter.createFeed(idOfSearch)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
launchUI {
|
|
||||||
filterSheet?.setSavedSearches(presenter.loadSearches())
|
|
||||||
}
|
|
||||||
filterSheet?.setFilters(presenter.filterItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
SourceFeedScreen(
|
Navigator(screen = SourceFeedScreen(sourceId))
|
||||||
presenter = presenter,
|
|
||||||
onFabClick = { filterSheet?.show() },
|
|
||||||
onClickBrowse = ::onBrowseClick,
|
|
||||||
onClickLatest = ::onLatestClick,
|
|
||||||
onClickSavedSearch = ::onSavedSearchClick,
|
|
||||||
onClickDelete = ::onRemoveClick,
|
|
||||||
onClickManga = ::onMangaClick,
|
|
||||||
onClickSearch = ::onSearchClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
BackHandler(presenter.searchQuery != null) {
|
|
||||||
presenter.searchQuery = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when manga in global search is clicked, opens manga.
|
|
||||||
*
|
|
||||||
* @param manga clicked item containing manga information.
|
|
||||||
*/
|
|
||||||
private fun onMangaClick(manga: Manga) {
|
|
||||||
// Open MangaController.
|
|
||||||
router.pushController(MangaController(manga.id, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) {
|
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLatestClick() {
|
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, GetRemoteManga.QUERY_LATEST).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onBrowseClick() {
|
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, GetRemoteManga.QUERY_POPULAR).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSavedSearchClick(savedSearch: SavedSearch) {
|
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, savedSearch = savedSearch.id).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSearchClick(query: String) {
|
|
||||||
onBrowseClick(query.nullIfBlank())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRemoveClick(feedSavedSearch: FeedSavedSearch) {
|
|
||||||
MaterialAlertDialogBuilder(activity!!)
|
|
||||||
.setTitle(R.string.feed)
|
|
||||||
.setMessage(R.string.feed_delete)
|
|
||||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
|
||||||
presenter.deleteFeed(feedSavedSearch)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -0,0 +1,214 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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.Navigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import com.bluelinelabs.conductor.Router
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||||
|
import eu.kanade.presentation.browse.SourceFeedScreen
|
||||||
|
import eu.kanade.presentation.browse.components.SourceFeedAddDialog
|
||||||
|
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
|
||||||
|
import eu.kanade.presentation.browse.components.SourceFeedFailedToLoadSavedSearchDialog
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
import exh.util.nullIfBlank
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||||
|
|
||||||
|
class SourceFeedScreen(val sourceId: Long) : Screen {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var filterSheet: SourceFilterSheet? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val screenModel = rememberScreenModel { SourceFeedScreenModel(sourceId) }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val router = LocalRouter.currentOrThrow
|
||||||
|
|
||||||
|
SourceFeedScreen(
|
||||||
|
name = screenModel.source.name,
|
||||||
|
isLoading = state.isLoading,
|
||||||
|
items = state.items,
|
||||||
|
onFabClick = if (state.filters.isEmpty()) null else { { filterSheet?.show() } },
|
||||||
|
onClickBrowse = { onBrowseClick(router, screenModel.source) },
|
||||||
|
onClickLatest = { onLatestClick(router, screenModel.source) },
|
||||||
|
onClickSavedSearch = { onSavedSearchClick(router, screenModel.source, it) },
|
||||||
|
onClickDelete = screenModel::openDeleteFeed,
|
||||||
|
onClickManga = { onMangaClick(navigator, it) },
|
||||||
|
onClickSearch = { onSearchClick(router, screenModel.source, it) },
|
||||||
|
searchQuery = state.searchQuery,
|
||||||
|
onSearchQueryChange = screenModel::search,
|
||||||
|
isIncognitoMode = screenModel.isIncognitoMode,
|
||||||
|
isDownloadOnly = screenModel.isDownloadOnly,
|
||||||
|
getMangaState = { screenModel.getManga(initialManga = it) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val onDismissRequest = screenModel::dismissDialog
|
||||||
|
when (val dialog = state.dialog) {
|
||||||
|
is SourceFeedScreenModel.Dialog.AddFeed -> {
|
||||||
|
SourceFeedAddDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
name = dialog.name,
|
||||||
|
addFeed = {
|
||||||
|
screenModel.createFeed(dialog.feedId)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SourceFeedScreenModel.Dialog.DeleteFeed -> {
|
||||||
|
SourceFeedDeleteDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
deleteFeed = {
|
||||||
|
screenModel.deleteFeed(dialog.feed)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SourceFeedScreenModel.Dialog.FailedToLoadSavedSearch -> {
|
||||||
|
SourceFeedFailedToLoadSavedSearchDialog(onDismissRequest)
|
||||||
|
}
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(state.searchQuery != null) {
|
||||||
|
screenModel.search(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(state.filters) {
|
||||||
|
initFilterSheet(state, screenModel, scope, context, router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initFilterSheet(
|
||||||
|
state: SourceFeedState,
|
||||||
|
screenModel: SourceFeedScreenModel,
|
||||||
|
viewScope: CoroutineScope,
|
||||||
|
context: Context,
|
||||||
|
router: Router,
|
||||||
|
) {
|
||||||
|
val filterSerializer = FilterSerializer()
|
||||||
|
filterSheet = SourceFilterSheet(
|
||||||
|
context,
|
||||||
|
// SY -->
|
||||||
|
router,
|
||||||
|
screenModel.source,
|
||||||
|
emptyList(),
|
||||||
|
// SY <--
|
||||||
|
onFilterClicked = {
|
||||||
|
val allDefault = state.filters == screenModel.source.getFilterList()
|
||||||
|
filterSheet?.dismiss()
|
||||||
|
if (allDefault) {
|
||||||
|
onBrowseClick(
|
||||||
|
router,
|
||||||
|
screenModel.source.id,
|
||||||
|
state.searchQuery?.nullIfBlank(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onBrowseClick(
|
||||||
|
router,
|
||||||
|
screenModel.source.id,
|
||||||
|
state.searchQuery?.nullIfBlank(),
|
||||||
|
filters = Json.encodeToString(filterSerializer.serialize(state.filters)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResetClicked = {},
|
||||||
|
onSaveClicked = {},
|
||||||
|
onSavedSearchClicked = { idOfSearch ->
|
||||||
|
viewScope.launchUI {
|
||||||
|
val search = screenModel.loadSearch(idOfSearch)
|
||||||
|
|
||||||
|
if (search == null) {
|
||||||
|
screenModel.openFailedToLoadSavedSearch()
|
||||||
|
return@launchUI
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search.filterList == null && state.filters.isNotEmpty()) {
|
||||||
|
context.toast(R.string.save_search_invalid)
|
||||||
|
return@launchUI
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search.filterList != null) {
|
||||||
|
screenModel.setFilters(FilterList(search.filterList))
|
||||||
|
filterSheet?.setFilters(state.filterItems)
|
||||||
|
}
|
||||||
|
val allDefault = search.filterList != null && state.filters == screenModel.source.getFilterList()
|
||||||
|
filterSheet?.dismiss()
|
||||||
|
|
||||||
|
if (!allDefault) {
|
||||||
|
onBrowseClick(
|
||||||
|
router,
|
||||||
|
screenModel.source.id,
|
||||||
|
search = state.searchQuery?.nullIfBlank(),
|
||||||
|
savedSearch = search.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSavedSearchDeleteClicked = { idOfSearch, name ->
|
||||||
|
viewScope.launchUI {
|
||||||
|
if (screenModel.hasTooManyFeeds()) {
|
||||||
|
context.toast(R.string.too_many_in_feed)
|
||||||
|
return@launchUI
|
||||||
|
}
|
||||||
|
screenModel.openAddFeed(idOfSearch, name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
viewScope.launchUI {
|
||||||
|
filterSheet?.setSavedSearches(screenModel.loadSearches())
|
||||||
|
}
|
||||||
|
filterSheet?.setFilters(state.filterItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMangaClick(navigator: Navigator, manga: Manga) {
|
||||||
|
navigator.push(MangaScreen(manga.id, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBrowseClick(router: Router, sourceId: Long, search: String? = null, savedSearch: Long? = null, filters: String? = null) {
|
||||||
|
router.replaceTopController(BrowseSourceController(sourceId, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLatestClick(router: Router, source: CatalogueSource) {
|
||||||
|
router.replaceTopController(BrowseSourceController(source, GetRemoteManga.QUERY_LATEST).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBrowseClick(router: Router, source: CatalogueSource) {
|
||||||
|
router.replaceTopController(BrowseSourceController(source, GetRemoteManga.QUERY_POPULAR).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSavedSearchClick(router: Router, source: CatalogueSource, savedSearch: SavedSearch) {
|
||||||
|
router.replaceTopController(BrowseSourceController(source, savedSearch = savedSearch.id).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSearchClick(router: Router, source: CatalogueSource, query: String) {
|
||||||
|
onBrowseClick(router, source.id, query.nullIfBlank())
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.core.prefs.asState
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.manga.interactor.GetManga
|
import eu.kanade.domain.manga.interactor.GetManga
|
||||||
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
|
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
|
||||||
@ -17,33 +21,32 @@ import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
|||||||
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
|
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
|
||||||
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
|
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
|
||||||
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
||||||
import eu.kanade.presentation.browse.SourceFeedState
|
|
||||||
import eu.kanade.presentation.browse.SourceFeedStateImpl
|
|
||||||
import eu.kanade.presentation.browse.SourceFeedUI
|
import eu.kanade.presentation.browse.SourceFeedUI
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
import exh.savedsearches.models.SavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
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.concurrent.Executors
|
||||||
import eu.kanade.domain.manga.model.Manga as DomainManga
|
import eu.kanade.domain.manga.model.Manga as DomainManga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,9 +55,9 @@ import eu.kanade.domain.manga.model.Manga as DomainManga
|
|||||||
*
|
*
|
||||||
* @param source the source.
|
* @param source the source.
|
||||||
*/
|
*/
|
||||||
open class SourceFeedPresenter(
|
open class SourceFeedScreenModel(
|
||||||
private val state: SourceFeedStateImpl = SourceFeedState() as SourceFeedStateImpl,
|
val sourceId: Long,
|
||||||
val source: CatalogueSource,
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val preferences: BasePreferences = Injekt.get(),
|
private val preferences: BasePreferences = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||||
@ -65,38 +68,33 @@ open class SourceFeedPresenter(
|
|||||||
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
||||||
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
||||||
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
|
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
|
||||||
) : BasePresenter<SourceFeedController>(), SourceFeedState by state {
|
) : StateScreenModel<SourceFeedState>(SourceFeedState()) {
|
||||||
|
|
||||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
val source = sourceManager.getOrStub(sourceId) as CatalogueSource
|
||||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
|
||||||
|
|
||||||
/**
|
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
|
||||||
* Fetches the different sources by user settings.
|
val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
|
||||||
*/
|
|
||||||
private var fetchSourcesSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
|
init {
|
||||||
setFilters(source.getFilterList())
|
setFilters(source.getFilterList())
|
||||||
|
|
||||||
getFeedSavedSearchBySourceId.subscribe(source.id)
|
getFeedSavedSearchBySourceId.subscribe(source.id)
|
||||||
.onEach {
|
.onEach {
|
||||||
val items = getSourcesToGetFeed(it)
|
val items = getSourcesToGetFeed(it)
|
||||||
state.items = items
|
mutableState.update { state ->
|
||||||
state.isLoading = false
|
state.copy(
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
}
|
||||||
getFeed(items)
|
getFeed(items)
|
||||||
}
|
}
|
||||||
.launchIn(presenterScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFilters(filters: FilterList) {
|
fun setFilters(filters: FilterList) {
|
||||||
state.filters = filters
|
mutableState.update { it.copy(filters = filters) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun hasTooManyFeeds(): Boolean {
|
suspend fun hasTooManyFeeds(): Boolean {
|
||||||
@ -104,7 +102,7 @@ open class SourceFeedPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFeed(savedSearchId: Long) {
|
fun createFeed(savedSearchId: Long) {
|
||||||
presenterScope.launchNonCancellable {
|
coroutineScope.launchNonCancellable {
|
||||||
insertFeedSavedSearch.await(
|
insertFeedSavedSearch.await(
|
||||||
FeedSavedSearch(
|
FeedSavedSearch(
|
||||||
id = -1,
|
id = -1,
|
||||||
@ -117,7 +115,7 @@ open class SourceFeedPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFeed(feed: FeedSavedSearch) {
|
fun deleteFeed(feed: FeedSavedSearch) {
|
||||||
presenterScope.launchNonCancellable {
|
coroutineScope.launchNonCancellable {
|
||||||
deleteFeedSavedSearchById.await(feed.id)
|
deleteFeedSavedSearchById.await(feed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,11 +139,10 @@ open class SourceFeedPresenter(
|
|||||||
* Initiates get manga per feed.
|
* Initiates get manga per feed.
|
||||||
*/
|
*/
|
||||||
private fun getFeed(feedSavedSearch: List<SourceFeedUI>) {
|
private fun getFeed(feedSavedSearch: List<SourceFeedUI>) {
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
coroutineScope.launch {
|
||||||
fetchSourcesSubscription = Observable.from(feedSavedSearch)
|
feedSavedSearch.forEach { sourceFeed ->
|
||||||
.flatMap(
|
val page = try {
|
||||||
{ sourceFeed ->
|
withContext(coroutineDispatcher) {
|
||||||
Observable.defer {
|
|
||||||
when (sourceFeed) {
|
when (sourceFeed) {
|
||||||
is SourceFeedUI.Browse -> source.fetchPopularManga(1)
|
is SourceFeedUI.Browse -> source.fetchPopularManga(1)
|
||||||
is SourceFeedUI.Latest -> source.fetchLatestUpdates(1)
|
is SourceFeedUI.Latest -> source.fetchLatestUpdates(1)
|
||||||
@ -154,30 +151,25 @@ open class SourceFeedPresenter(
|
|||||||
query = sourceFeed.savedSearch.query.orEmpty(),
|
query = sourceFeed.savedSearch.query.orEmpty(),
|
||||||
filters = getFilterList(sourceFeed.savedSearch, source),
|
filters = getFilterList(sourceFeed.savedSearch, source),
|
||||||
)
|
)
|
||||||
}
|
}.awaitSingle()
|
||||||
|
}.mangas
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val titles = page.map {
|
||||||
|
withIOContext {
|
||||||
|
networkToLocalManga.await(it.toDomainManga(source.id))
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
}
|
||||||
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
|
||||||
.map { it.mangas } // Get manga from search result.
|
mutableState.update { state ->
|
||||||
.map { list -> runBlocking { list.map { networkToLocalManga.await(it.toDomainManga(source.id)) } } } // Convert to local manga.
|
state.copy(
|
||||||
.map { list -> sourceFeed.withResults(list) }
|
items = state.items.map { item -> if (item.id == sourceFeed.id) sourceFeed.withResults(titles) else item },
|
||||||
},
|
)
|
||||||
5,
|
|
||||||
)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
// Update matching source with the obtained results
|
|
||||||
.doOnNext { result ->
|
|
||||||
synchronized(state) {
|
|
||||||
state.items = state.items?.map { item -> if (item.id == result.id) result else item }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deliver initial state
|
}
|
||||||
.subscribe(
|
|
||||||
{},
|
|
||||||
{ error ->
|
|
||||||
logcat(LogPriority.ERROR, error)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val filterSerializer = FilterSerializer()
|
private val filterSerializer = FilterSerializer()
|
||||||
@ -233,4 +225,48 @@ open class SourceFeedPresenter(
|
|||||||
|
|
||||||
suspend fun loadSearches() =
|
suspend fun loadSearches() =
|
||||||
getExhSavedSearch.await(source.id, source::getFilterList)
|
getExhSavedSearch.await(source.id, source::getFilterList)
|
||||||
|
|
||||||
|
fun search(query: String?) {
|
||||||
|
mutableState.update { it.copy(searchQuery = query) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDeleteFeed(feed: FeedSavedSearch) {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.DeleteFeed(feed)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openAddFeed(feedId: Long, name: String) {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.AddFeed(feedId, name)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openFailedToLoadSavedSearch() {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Dialog {
|
||||||
|
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
|
||||||
|
data class AddFeed(val feedId: Long, val name: String) : Dialog()
|
||||||
|
object FailedToLoadSavedSearch : Dialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDispose() {
|
||||||
|
super.onDispose()
|
||||||
|
coroutineDispatcher.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class SourceFeedState(
|
||||||
|
val searchQuery: String? = null,
|
||||||
|
val items: List<SourceFeedUI> = emptyList(),
|
||||||
|
val filters: FilterList = FilterList(),
|
||||||
|
val dialog: SourceFeedScreenModel.Dialog? = null,
|
||||||
|
) {
|
||||||
|
val filterItems: List<IFlexible<*>> by lazy { filters.toItems() }
|
||||||
|
|
||||||
|
val isLoading
|
||||||
|
get() = items.isEmpty()
|
||||||
}
|
}
|
@ -64,6 +64,7 @@ import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController.Companion.SMART_SEARCH_SOURCE_TAG
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesController.Companion.SMART_SEARCH_SOURCE_TAG
|
||||||
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.feed.SourceFeedController
|
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.history.HistoryController
|
import eu.kanade.tachiyomi.ui.history.HistoryController
|
||||||
@ -160,11 +161,11 @@ class MangaScreen(
|
|||||||
// SY <--
|
// SY <--
|
||||||
onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||||
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
|
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
|
||||||
onTagClicked = { performGenreSearch(router, it, screenModel.source!!) },
|
onTagClicked = { performGenreSearch(router, navigator, it, screenModel.source!!) },
|
||||||
onFilterButtonClicked = screenModel::showSettingsDialog,
|
onFilterButtonClicked = screenModel::showSettingsDialog,
|
||||||
onRefresh = screenModel::fetchAllFromSource,
|
onRefresh = screenModel::fetchAllFromSource,
|
||||||
onContinueReading = { continueReading(context, screenModel.getNextUnreadChapter()) },
|
onContinueReading = { continueReading(context, screenModel.getNextUnreadChapter()) },
|
||||||
onSearch = { query, global -> performSearch(router, query, global) },
|
onSearch = { query, global -> performSearch(router, navigator, query, global) },
|
||||||
onCoverClicked = screenModel::showCoverDialog,
|
onCoverClicked = screenModel::showCoverDialog,
|
||||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||||
@ -368,12 +369,25 @@ class MangaScreen(
|
|||||||
*
|
*
|
||||||
* @param query the search query to the parent controller
|
* @param query the search query to the parent controller
|
||||||
*/
|
*/
|
||||||
private fun performSearch(router: Router, query: String, global: Boolean) {
|
private fun performSearch(router: Router, navigator: Navigator, query: String, global: Boolean) {
|
||||||
if (global) {
|
if (global) {
|
||||||
router.pushController(GlobalSearchController(query))
|
router.pushController(GlobalSearchController(query))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
if (navigator.canPop) {
|
||||||
|
when (val previousScreen = navigator.items[navigator.items.size - 2]) {
|
||||||
|
is SourceFeedScreen -> {
|
||||||
|
navigator.pop()
|
||||||
|
previousScreen.onBrowseClick(router, previousScreen.sourceId, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
if (router.backstackSize < 2) {
|
if (router.backstackSize < 2) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -399,7 +413,8 @@ class MangaScreen(
|
|||||||
// SY -->
|
// SY -->
|
||||||
is SourceFeedController -> {
|
is SourceFeedController -> {
|
||||||
router.handleBack()
|
router.handleBack()
|
||||||
previousController.onBrowseClick(query)
|
router.handleBack()
|
||||||
|
router.pushController(BrowseSourceController(previousController.sourceId, query))
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@ -410,7 +425,7 @@ class MangaScreen(
|
|||||||
*
|
*
|
||||||
* @param genreName the search genre to the parent controller
|
* @param genreName the search genre to the parent controller
|
||||||
*/
|
*/
|
||||||
private fun performGenreSearch(router: Router, genreName: String, source: Source) {
|
private fun performGenreSearch(router: Router, navigator: Navigator, genreName: String, source: Source) {
|
||||||
if (router.backstackSize < 2) {
|
if (router.backstackSize < 2) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -423,7 +438,7 @@ class MangaScreen(
|
|||||||
router.handleBack()
|
router.handleBack()
|
||||||
previousController.searchWithGenre(genreName)
|
previousController.searchWithGenre(genreName)
|
||||||
} else {
|
} else {
|
||||||
performSearch(router, genreName, global = false)
|
performSearch(router, navigator, genreName, global = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,19 +4,17 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bluelinelabs.conductor.Router
|
||||||
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
|
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import exh.md.follows.MangaDexFollowsController
|
import exh.md.follows.MangaDexFollowsController
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
|
||||||
|
|
||||||
class MangaDexFabHeaderAdapter(val controller: BaseController<*>, val source: CatalogueSource, val onClick: () -> Unit) :
|
class MangaDexFabHeaderAdapter(val router: Router, val source: CatalogueSource, val onClick: () -> Unit) :
|
||||||
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
|
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
|
||||||
|
|
||||||
private lateinit var binding: SourceFilterMangadexHeaderBinding
|
private lateinit var binding: SourceFilterMangadexHeaderBinding
|
||||||
@ -35,22 +33,23 @@ class MangaDexFabHeaderAdapter(val controller: BaseController<*>, val source: Ca
|
|||||||
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
fun bind() {
|
fun bind() {
|
||||||
binding.mangadexFollows.setOnClickListener {
|
binding.mangadexFollows.setOnClickListener {
|
||||||
controller.router.replaceTopController(MangaDexFollowsController(source).withFadeTransaction())
|
router.replaceTopController(MangaDexFollowsController(source).withFadeTransaction())
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
binding.mangadexRandom.clicks()
|
binding.mangadexRandom.setOnClickListener {
|
||||||
.onEach {
|
launchUI {
|
||||||
val randomMangaUrl = withIOContext {
|
val randomMangaUrl = withIOContext {
|
||||||
(source as? RandomMangaSource)?.fetchRandomMangaUrl()
|
(source as? RandomMangaSource)?.fetchRandomMangaUrl()
|
||||||
}
|
}
|
||||||
controller.router.replaceTopController(
|
router.replaceTopController(
|
||||||
BrowseSourceController(
|
BrowseSourceController(
|
||||||
source,
|
source,
|
||||||
"id:$randomMangaUrl",
|
"id:$randomMangaUrl",
|
||||||
).withFadeTransaction(),
|
).withFadeTransaction(),
|
||||||
)
|
)
|
||||||
onClick()
|
onClick()
|
||||||
}.launchIn(controller.viewScope)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user