feat: batch processing for recommendations & sort by relevancy (#1383)
* refactor: use NoResultsException * refactor: cleanup RecommendationPagingSources * refactor: turn wake/wifi lock functions into reusable extensions * feat: implement batch recommendation (initial version) * fix: serialization issues * fix: wrong source id * refactor: increase performance using virtual paging * refactor: update string * refactor: handle 404 of MD source correctly * style: add newline * refactor: create universal throttle manager * refactor: throttle requests * chore: remove unused strings * feat: rank recommendations by match count * feat: add badges indicating match count to batch recommendations * fix: handle rec search with no results * fix: validate flags in pre-search bottom sheet * feat: implement 'hide library entries' for recommendation search using custom SmartSearchEngine for library items * style: run spotless * fix: cancel button * fix: racing condition causing loss of state
This commit is contained in:
+9
@@ -19,6 +19,7 @@ import eu.kanade.presentation.library.components.MangaComfortableGridItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.RankedSearchMetadata
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -119,6 +120,14 @@ private fun BrowseSourceComfortableGridItem(
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
} else if (metadata is RankedSearchMetadata) {
|
||||
metadata.rank?.let {
|
||||
Badge(
|
||||
text = "+$it",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
// SY <--
|
||||
|
||||
@@ -19,6 +19,7 @@ import eu.kanade.presentation.library.components.MangaCompactGridItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.RankedSearchMetadata
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -119,6 +120,14 @@ private fun BrowseSourceCompactGridItem(
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
} else if (metadata is RankedSearchMetadata) {
|
||||
metadata.rank?.let {
|
||||
Badge(
|
||||
text = "+$it",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
// SY <--
|
||||
|
||||
@@ -16,6 +16,7 @@ import eu.kanade.presentation.library.components.MangaListItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.RankedSearchMetadata
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -110,6 +111,14 @@ private fun BrowseSourceListItem(
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
} else if (metadata is RankedSearchMetadata) {
|
||||
metadata.rank?.let {
|
||||
Badge(
|
||||
text = "+$it",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
},
|
||||
|
||||
@@ -28,7 +28,6 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.RemoveDone
|
||||
import androidx.compose.material.icons.outlined.SwapCalls
|
||||
@@ -237,6 +236,7 @@ fun LibraryBottomActionMenu(
|
||||
// SY -->
|
||||
onClickCleanTitles: (() -> Unit)?,
|
||||
onClickMigrate: (() -> Unit)?,
|
||||
onClickCollectRecommendations: (() -> Unit)?,
|
||||
onClickAddToMangaDex: (() -> Unit)?,
|
||||
onClickResetInfo: (() -> Unit)?,
|
||||
// SY <--
|
||||
@@ -267,7 +267,10 @@ fun LibraryBottomActionMenu(
|
||||
}
|
||||
}
|
||||
// SY -->
|
||||
val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null || onClickResetInfo != null
|
||||
val showOverflow = onClickCleanTitles != null ||
|
||||
onClickAddToMangaDex != null ||
|
||||
onClickResetInfo != null ||
|
||||
onClickCollectRecommendations != null
|
||||
val configuration = LocalConfiguration.current
|
||||
val moveMarkPrev = remember { !configuration.isTabletUi() }
|
||||
var overFlowOpen by remember { mutableStateOf(false) }
|
||||
@@ -358,6 +361,12 @@ fun LibraryBottomActionMenu(
|
||||
onClick = onClickMigrate,
|
||||
)
|
||||
}
|
||||
if (onClickCollectRecommendations != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.rec_search_short)) },
|
||||
onClick = onClickCollectRecommendations,
|
||||
)
|
||||
}
|
||||
if (onClickAddToMangaDex != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.mangadex_add_to_follows)) },
|
||||
|
||||
+4
-4
@@ -17,9 +17,9 @@ import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.eh.EHentaiThrottleManager
|
||||
import exh.smartsearch.SmartSearchEngine
|
||||
import exh.smartsearch.SmartSourceSearchEngine
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.util.ThrottleManager
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -84,8 +84,8 @@ class MigrationListScreenModel(
|
||||
private val deleteTrack: DeleteTrack = Injekt.get(),
|
||||
) : ScreenModel {
|
||||
|
||||
private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams)
|
||||
private val throttleManager = EHentaiThrottleManager()
|
||||
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
|
||||
private val throttleManager = ThrottleManager()
|
||||
|
||||
val migratingItems = MutableStateFlow<ImmutableList<MigratingManga>?>(null)
|
||||
val migrationDone = MutableStateFlow(false)
|
||||
|
||||
@@ -42,6 +42,7 @@ import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.sql.models.SearchTag
|
||||
import exh.metadata.sql.models.SearchTitle
|
||||
import exh.recs.batch.RecommendationSearchHelper
|
||||
import exh.search.Namespace
|
||||
import exh.search.QueryComponent
|
||||
import exh.search.SearchEngine
|
||||
@@ -61,6 +62,7 @@ import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -160,6 +162,9 @@ class LibraryScreenModel(
|
||||
|
||||
// SY -->
|
||||
val favoritesSync = FavoritesSyncHelper(preferences.context)
|
||||
val recommendationSearch = RecommendationSearchHelper(preferences.context)
|
||||
|
||||
private var recommendationSearchJob: Job? = null
|
||||
// SY <--
|
||||
|
||||
init {
|
||||
@@ -898,6 +903,10 @@ class LibraryScreenModel(
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun showRecommendationSearchDialog() {
|
||||
val mangaList = state.value.selection.map { it.manga }
|
||||
mutableState.update { it.copy(dialog = Dialog.RecommendationSearchSheet(mangaList)) }
|
||||
}
|
||||
|
||||
fun getCategoryName(
|
||||
context: Context,
|
||||
@@ -1222,8 +1231,12 @@ class LibraryScreenModel(
|
||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
) : Dialog
|
||||
data class DeleteManga(val manga: List<Manga>) : Dialog
|
||||
|
||||
// SY -->
|
||||
data object SyncFavoritesWarning : Dialog
|
||||
data object SyncFavoritesConfirm : Dialog
|
||||
data class RecommendationSearchSheet(val manga: List<Manga>) : Dialog
|
||||
// SY <--
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -1316,6 +1329,16 @@ class LibraryScreenModel(
|
||||
}.toSortedMap(compareBy { it.order })
|
||||
}
|
||||
|
||||
fun runRecommendationSearch(selection: List<Manga>) {
|
||||
recommendationSearch.runSearch(screenModelScope, selection)?.let {
|
||||
recommendationSearchJob = it
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelRecommendationSearch() {
|
||||
recommendationSearchJob?.cancel()
|
||||
}
|
||||
|
||||
fun runSync() {
|
||||
favoritesSync.runSync(screenModelScope)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.favorites.FavoritesSyncStatus
|
||||
import exh.recs.RecommendsScreen
|
||||
import exh.recs.batch.RecommendationSearchBottomSheetDialog
|
||||
import exh.recs.batch.RecommendationSearchProgressDialog
|
||||
import exh.recs.batch.SearchStatus
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -205,6 +209,7 @@ data object LibraryTab : Tab {
|
||||
context.toast(SYMR.strings.no_valid_entry)
|
||||
}
|
||||
},
|
||||
onClickCollectRecommendations = screenModel::showRecommendationSearchDialog.takeIf { state.selection.size > 1 },
|
||||
onClickAddToMangaDex = screenModel::syncMangaToDex.takeIf { state.showAddToMangadex },
|
||||
onClickResetInfo = screenModel::resetInfo.takeIf { state.showResetInfo },
|
||||
// SY <--
|
||||
@@ -310,6 +315,7 @@ data object LibraryTab : Tab {
|
||||
},
|
||||
)
|
||||
}
|
||||
// SY -->
|
||||
LibraryScreenModel.Dialog.SyncFavoritesWarning -> {
|
||||
SyncFavoritesWarningDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@@ -328,6 +334,17 @@ data object LibraryTab : Tab {
|
||||
},
|
||||
)
|
||||
}
|
||||
is LibraryScreenModel.Dialog.RecommendationSearchSheet -> {
|
||||
RecommendationSearchBottomSheetDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onSearchRequest = {
|
||||
onDismissRequest()
|
||||
screenModel.clearSelection()
|
||||
screenModel.runRecommendationSearch(dialog.manga)
|
||||
},
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
null -> {}
|
||||
}
|
||||
|
||||
@@ -337,6 +354,12 @@ data object LibraryTab : Tab {
|
||||
setStatusIdle = { screenModel.favoritesSync.status.value = FavoritesSyncStatus.Idle },
|
||||
openManga = { navigator.push(MangaScreen(it)) },
|
||||
)
|
||||
|
||||
RecommendationSearchProgressDialog(
|
||||
status = screenModel.recommendationSearch.status.collectAsState().value,
|
||||
setStatusIdle = { screenModel.recommendationSearch.status.value = SearchStatus.Idle },
|
||||
setStatusCancelling = { screenModel.recommendationSearch.status.value = SearchStatus.Cancelling },
|
||||
)
|
||||
// SY <--
|
||||
|
||||
BackHandler(enabled = state.selectionMode || state.searchQuery != null) {
|
||||
@@ -356,6 +379,30 @@ data object LibraryTab : Tab {
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
val recSearchState by screenModel.recommendationSearch.status.collectAsState()
|
||||
LaunchedEffect(recSearchState) {
|
||||
when (val current = recSearchState) {
|
||||
is SearchStatus.Finished.WithResults -> {
|
||||
RecommendsScreen.Args.MergedSourceMangas(current.results)
|
||||
.let(::RecommendsScreen)
|
||||
.let(navigator::push)
|
||||
|
||||
screenModel.recommendationSearch.status.value = SearchStatus.Idle
|
||||
}
|
||||
is SearchStatus.Finished.WithoutResults -> {
|
||||
context.toast(SYMR.strings.rec_no_results)
|
||||
screenModel.recommendationSearch.status.value = SearchStatus.Idle
|
||||
}
|
||||
is SearchStatus.Cancelling -> {
|
||||
screenModel.cancelRecommendationSearch()
|
||||
screenModel.recommendationSearch.status.value = SearchStatus.Idle
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch { queryEvent.receiveAsFlow().collect(screenModel::search) }
|
||||
launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { screenModel.showSettingsDialog() } }
|
||||
|
||||
@@ -548,7 +548,9 @@ class MangaScreen(
|
||||
// AZ -->
|
||||
private fun openRecommends(navigator: Navigator, source: Source?, manga: Manga) {
|
||||
source ?: return
|
||||
navigator.push(RecommendsScreen(manga.id, source.id))
|
||||
RecommendsScreen.Args.SingleSourceManga(manga.id, source.id)
|
||||
.let(::RecommendsScreen)
|
||||
.let(navigator::push)
|
||||
}
|
||||
// AZ <--
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user