Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: batch processing for recommendations & sort by relevancy #1383

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
11c35fd
refactor: use NoResultsException
timschneeb Jan 25, 2025
a2e9711
refactor: cleanup RecommendationPagingSources
timschneeb Jan 25, 2025
9d1f4c2
refactor: turn wake/wifi lock functions into reusable extensions
timschneeb Jan 25, 2025
e2306d4
feat: implement batch recommendation (initial version)
timschneeb Jan 25, 2025
7cb12b5
fix: serialization issues
timschneeb Jan 25, 2025
77ec12e
fix: wrong source id
timschneeb Jan 25, 2025
1622ef4
refactor: increase performance using virtual paging
timschneeb Jan 25, 2025
318cfbe
refactor: update string
timschneeb Jan 25, 2025
6fdff4c
refactor: handle 404 of MD source correctly
timschneeb Jan 25, 2025
898c3c8
style: add newline
timschneeb Jan 25, 2025
113d985
refactor: create universal throttle manager
timschneeb Jan 25, 2025
45cc667
refactor: throttle requests
timschneeb Jan 25, 2025
a10094e
chore: remove unused strings
timschneeb Jan 25, 2025
28563ff
feat: rank recommendations by match count
timschneeb Jan 26, 2025
b6eb263
feat: add badges indicating match count to batch recommendations
timschneeb Jan 26, 2025
5cef0d4
fix: handle rec search with no results
timschneeb Jan 26, 2025
0e3c269
fix: validate flags in pre-search bottom sheet
timschneeb Jan 26, 2025
a23636a
feat: implement 'hide library entries' for recommendation search usin…
timschneeb Jan 26, 2025
6270f47
style: run spotless
timschneeb Jan 26, 2025
3ceac97
fix: cancel button
timschneeb Jan 26, 2025
8b6dda8
fix: racing condition causing loss of state
timschneeb Jan 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <--
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -237,6 +236,7 @@ fun LibraryBottomActionMenu(
// SY -->
onClickCleanTitles: (() -> Unit)?,
onClickMigrate: (() -> Unit)?,
onClickCollectRecommendations: (() -> Unit)?,
onClickAddToMangaDex: (() -> Unit)?,
onClickResetInfo: (() -> Unit)?,
// SY <--
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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)) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -160,6 +162,9 @@ class LibraryScreenModel(

// SY -->
val favoritesSync = FavoritesSyncHelper(preferences.context)
val recommendationSearch = RecommendationSearchHelper(preferences.context)

private var recommendationSearchJob: Job? = null
// SY <--

init {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 -->
Expand Down Expand Up @@ -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)
}
Expand Down
47 changes: 47 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <--
Expand Down Expand Up @@ -310,6 +315,7 @@ data object LibraryTab : Tab {
},
)
}
// SY -->
LibraryScreenModel.Dialog.SyncFavoritesWarning -> {
SyncFavoritesWarningDialog(
onDismissRequest = onDismissRequest,
Expand All @@ -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 -> {}
}

Expand All @@ -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) {
Expand All @@ -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() } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <--
}
4 changes: 2 additions & 2 deletions app/src/main/java/exh/debug/DebugFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.source.AndroidSourceManager
import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.util.system.workManager
import exh.eh.EHentaiThrottleManager
import exh.eh.EHentaiUpdateWorker
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import exh.source.nHentaiSourceIds
import exh.util.ThrottleManager
import exh.util.jobScheduler
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator
Expand Down Expand Up @@ -76,7 +76,7 @@ object DebugFunctions {
}
}
}
private val throttleManager = EHentaiThrottleManager()
private val throttleManager = ThrottleManager()

fun getDelegatedSourceList(): String = AndroidSourceManager.currentDelegatedSources.map {
it.value.sourceName + " : " + it.value.sourceId + " : " + it.value.factory
Expand Down
31 changes: 6 additions & 25 deletions app/src/main/java/exh/favorites/FavoritesSyncHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ package exh.favorites

import android.content.Context
import android.net.wifi.WifiManager
import android.os.Build
import android.os.PowerManager
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast
import exh.GalleryAddEvent
import exh.GalleryAdder
import exh.eh.EHentaiThrottleManager
import exh.eh.EHentaiUpdateWorker
import exh.log.xLog
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import exh.source.isEhBasedManga
import exh.util.ThrottleManager
import exh.util.createPartialWakeLock
import exh.util.createWifiLock
import exh.util.ignore
import exh.util.wifiManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -69,7 +68,7 @@ class FavoritesSyncHelper(val context: Context) {

private val galleryAdder by lazy { GalleryAdder() }

private val throttleManager by lazy { EHentaiThrottleManager() }
private val throttleManager by lazy { ThrottleManager() }

private var wifiLock: WifiManager.WifiLock? = null
private var wakeLock: PowerManager.WakeLock? = null
Expand Down Expand Up @@ -130,27 +129,9 @@ class FavoritesSyncHelper(val context: Context) {
try {
// Take wake + wifi locks
ignore { wakeLock?.release() }
wakeLock = ignore {
context.powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"teh:ExhFavoritesSyncWakelock",
)
}
wakeLock = ignore { context.createPartialWakeLock("teh:ExhFavoritesSyncWakelock") }
ignore { wifiLock?.release() }
wifiLock = ignore {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.wifiManager.createWifiLock(
WifiManager.WIFI_MODE_FULL_LOW_LATENCY,
"teh:ExhFavoritesSyncWifi",
)
} else {
@Suppress("DEPRECATION")
context.wifiManager.createWifiLock(
WifiManager.WIFI_MODE_FULL_HIGH_PERF,
"teh:ExhFavoritesSyncWifi",
)
}
}
wifiLock = ignore { context.createWifiLock("teh:ExhFavoritesSyncWifi") }

// Do not update galleries while syncing favorites
EHentaiUpdateWorker.cancelBackground(context)
Expand Down
Loading
Loading