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

Support filters (tags) in serach #1817

Merged
merged 8 commits into from
Mar 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -141,6 +141,10 @@ interface AniNavigator {
currentNavigator.navigate(NavRoutes.Settings(tab))
}

fun navigateSubjectSearch() {
currentNavigator.navigate(NavRoutes.SubjectSearch)
}

fun navigateEditMediaSource(
factoryId: FactoryId,
mediaSourceInstanceId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ package me.him188.ani.app.navigation

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DownloadDone
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.TravelExplore
import androidx.compose.runtime.Immutable
Expand All @@ -37,7 +36,7 @@ sealed class NavRoutes {
*/
val popUpTargetInclusive: NavRoutes? = null,
) : NavRoutes()

@Serializable
data class OnboardingComplete(
/**
Expand Down Expand Up @@ -65,6 +64,9 @@ sealed class NavRoutes {
val tab: SettingsTab? = null,
) : NavRoutes()

@Serializable
data object SubjectSearch : NavRoutes()

@Serializable
data class SubjectDetail(
val subjectId: Int,
Expand Down Expand Up @@ -124,11 +126,11 @@ enum class MainScreenPage {
Exploration,
Collection,
CacheManagement,
Search, ;
;

companion object {
@Stable
val visibleEntries by lazy(LazyThreadSafetyMode.PUBLICATION) { entries - Search }
val visibleEntries get() = entries

@Stable
val NavType by lazy(LazyThreadSafetyMode.PUBLICATION) {
Expand Down Expand Up @@ -170,13 +172,11 @@ fun MainScreenPage.getIcon() = when (this) {
MainScreenPage.Exploration -> Icons.Rounded.TravelExplore
MainScreenPage.Collection -> Icons.Rounded.Star
MainScreenPage.CacheManagement -> Icons.Rounded.DownloadDone
MainScreenPage.Search -> Icons.Rounded.Search
}

@Stable
fun MainScreenPage.getText(): String = when (this) {
MainScreenPage.Exploration -> "探索"
MainScreenPage.Collection -> "追番"
MainScreenPage.CacheManagement -> "缓存"
MainScreenPage.Search -> "搜索"
}
18 changes: 18 additions & 0 deletions app/shared/src/commonMain/kotlin/ui/main/AniAppContent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import me.him188.ani.app.ui.onboarding.WelcomeScreen
import me.him188.ani.app.ui.profile.auth.AniContactList
import me.him188.ani.app.ui.profile.auth.BangumiAuthorizePage
import me.him188.ani.app.ui.profile.auth.BangumiAuthorizeViewModel
import me.him188.ani.app.ui.search.SearchScreen
import me.him188.ani.app.ui.settings.SettingsScreen
import me.him188.ani.app.ui.settings.SettingsViewModel
import me.him188.ani.app.ui.settings.mediasource.rss.EditRssMediaSourceScreen
Expand Down Expand Up @@ -265,11 +266,28 @@ private fun AniAppContentImpl(
authState = authState,
onNavigateToPage = { currentPage = it },
onNavigateToSettings = { aniNavigator.navigateSettings() },
onNavigateToSearch = { aniNavigator.navigateSubjectSearch() },
navigationLayoutType = navigationLayoutType,
)
}
}
}
composable<NavRoutes.SubjectSearch>(
enterTransition = enterTransition,
exitTransition = exitTransition,
popEnterTransition = popEnterTransition,
popExitTransition = popExitTransition,
) {
val navigator = LocalNavigator.current
SearchScreen(
onNavigateBack = {
aniNavigator.popBackStack()
},
onNavigateToEpisodeDetails = { subjectId, episodeId ->
navigator.navigateEpisodeDetails(subjectId, episodeId)
},
)
}
composable<NavRoutes.BangumiAuthorize>(
enterTransition = enterTransition,
exitTransition = exitTransition,
Expand Down
79 changes: 14 additions & 65 deletions app/shared/src/commonMain/kotlin/ui/main/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Search
Expand All @@ -26,8 +25,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable
Expand All @@ -39,10 +36,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import me.him188.ani.app.domain.session.AuthState
import me.him188.ani.app.navigation.LocalNavigator
Expand All @@ -56,7 +51,6 @@ import me.him188.ani.app.ui.adaptive.navigation.AniNavigationSuiteLayout
import me.him188.ani.app.ui.cache.CacheManagementScreen
import me.him188.ani.app.ui.cache.CacheManagementViewModel
import me.him188.ani.app.ui.exploration.ExplorationScreen
import me.him188.ani.app.ui.exploration.search.SearchPage
import me.him188.ani.app.ui.foundation.LocalPlatform
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.ifThen
Expand All @@ -68,12 +62,9 @@ import me.him188.ani.app.ui.foundation.layout.desktopTitleBarPadding
import me.him188.ani.app.ui.foundation.layout.isHeightAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.isTopRight
import me.him188.ani.app.ui.foundation.layout.setRequestFullScreen
import me.him188.ani.app.ui.foundation.navigation.BackHandler
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
import me.him188.ani.app.ui.subject.collection.CollectionPage
import me.him188.ani.app.ui.subject.collection.UserCollectionsViewModel
import me.him188.ani.app.ui.subject.details.SubjectDetailsScene
import me.him188.ani.app.ui.update.TextButtonUpdateLogo
import me.him188.ani.utils.platform.isAndroid

Expand All @@ -85,6 +76,7 @@ fun MainScreen(
modifier: Modifier = Modifier,
onNavigateToPage: (MainScreenPage) -> Unit,
onNavigateToSettings: () -> Unit,
onNavigateToSearch: () -> Unit,
navigationLayoutType: NavigationSuiteType = AniNavigationSuiteDefaults.calculateLayoutType(
currentWindowAdaptiveInfo1(),
),
Expand All @@ -97,7 +89,15 @@ fun MainScreen(
}
}

MainScreenContent(page, authState, onNavigateToPage, onNavigateToSettings, modifier, navigationLayoutType)
MainScreenContent(
page,
authState,
onNavigateToPage,
onNavigateToSettings,
onNavigateToSearch,
modifier,
navigationLayoutType,
)
}

@Composable
Expand All @@ -106,6 +106,7 @@ private fun MainScreenContent(
authState: AuthState,
onNavigateToPage: (MainScreenPage) -> Unit,
onNavigateToSettings: () -> Unit,
onNavigateToSearch: () -> Unit,
modifier: Modifier = Modifier,
navigationLayoutType: NavigationSuiteType = AniNavigationSuiteDefaults.calculateLayoutType(
currentWindowAdaptiveInfo1(),
Expand All @@ -122,7 +123,7 @@ private fun MainScreenContent(
),
navigationRailHeader = {
FloatingActionButton(
{ onNavigateToPage(MainScreenPage.Search) },
onNavigateToSearch,
Modifier
.desktopTitleBarPadding()
.ifThen(currentWindowAdaptiveInfo1().windowSizeClass.isHeightAtLeastMedium) {
Expand Down Expand Up @@ -191,7 +192,7 @@ private fun MainScreenContent(
ExplorationScreen(
vm.explorationPageState,
authState,
onSearch = { onNavigateToPage(MainScreenPage.Search) },
onSearch = onNavigateToSearch,
onClickSettings = { navigator.navigateSettings() },
onClickLogin = { navigator.navigateBangumiAuthorize() },
onClickRetryRefreshSession = {
Expand All @@ -210,7 +211,7 @@ private fun MainScreenContent(
state = vm.state,
authState = authState,
items = vm.items.collectAsLazyPagingItems(),
onClickSearch = { onNavigateToPage(MainScreenPage.Search) },
onClickSearch = onNavigateToSearch,
onClickSettings = { navigator.navigateSettings() },
onClickLogin = { navigator.navigateBangumiAuthorize() },
onClickRetryRefreshSession = {
Expand All @@ -230,58 +231,6 @@ private fun MainScreenContent(
navigationIcon = { },
Modifier.fillMaxSize(),
)

MainScreenPage.Search -> {
val vm = viewModel { SearchViewModel() }
val onBack = {
onNavigateToPage(MainScreenPage.Exploration)
}
BackHandler(true, onBack)
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator()
SearchPage(
vm.searchPageState,
detailContent = {
val subjectDetailsState by vm.subjectDetailsStateLoader.state
.collectAsStateWithLifecycle(null)
SubjectDetailsScene(
subjectDetailsState,
authState,
onPlay = { episodeId ->
val current = subjectDetailsState
if (current != null) {
navigator.navigateEpisodeDetails(current.subjectId, episodeId)
}
},
onLoadErrorRetry = { vm.reloadCurrentSubjectDetails() },
navigationIcon = {
// 只有在单面板模式下才显示返回按钮
if (listDetailLayoutParameters.isSinglePane) {
BackNavigationIconButton(
onNavigateBack = {
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
listDetailNavigator.navigateBack()
}
},
)
}
},
)
},
Modifier.fillMaxSize(),
onSelect = { index, item ->
vm.searchPageState.selectedItemIndex = index
vm.viewSubjectDetails(item)
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
}
},
navigator = listDetailNavigator,
contentWindowInsets = WindowInsets.safeDrawing, // 不包含 macos 标题栏, 因为左侧有 navigation rail
navigationIcon = {
BackNavigationIconButton(onBack)
},
)
}
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions app/shared/src/commonMain/kotlin/ui/main/SearchViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import me.him188.ani.app.data.repository.subject.SubjectSearchHistoryRepository
import me.him188.ani.app.data.repository.subject.SubjectSearchRepository
import me.him188.ani.app.data.repository.user.SettingsRepository
import me.him188.ani.app.domain.search.SubjectSearchQuery
import me.him188.ani.app.domain.session.AniAuthStateProvider
import me.him188.ani.app.ui.exploration.search.SearchPageState
import me.him188.ani.app.ui.exploration.search.SubjectPreviewItemInfo
import me.him188.ani.app.ui.foundation.AbstractViewModel
Expand All @@ -46,19 +47,21 @@ class SearchViewModel : AbstractViewModel(), KoinComponent {
private val subjectSearchRepository: SubjectSearchRepository by inject()
private val subjectDetailsStateFactory: SubjectDetailsStateFactory by inject()
private val settingsRepository: SettingsRepository by inject()
private val authStateProvider: AniAuthStateProvider by inject()

private val nsfwSettingFlow = settingsRepository.uiSettings.flow.map { it.searchSettings.nsfwMode }

private val queryFlow = MutableStateFlow(SubjectSearchQuery(""))

val authState = authStateProvider.state
val searchPageState: SearchPageState = SearchPageState(
searchHistoryPager = searchHistoryRepository.getHistoryPager(),
suggestionsPager = queryFlow.debounce(200.milliseconds).flatMapLatest {
bangumiSubjectSearchCompletionRepository.completionsFlow(it.keywords)
},
queryFlow = queryFlow.map { it.keywords },
queryFlow = queryFlow,
setQuery = { newQuery ->
queryFlow.update { it.copy(keywords = newQuery) }
queryFlow.update { newQuery }
},
onRequestPlay = { info ->
episodeCollectionRepository.subjectEpisodeCollectionInfosFlow(info.subjectId).first().firstOrNull()?.let {
Expand Down
82 changes: 82 additions & 0 deletions app/shared/src/commonMain/kotlin/ui/search/SearchScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2024-2025 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.ui.search

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import me.him188.ani.app.domain.session.AuthState
import me.him188.ani.app.ui.exploration.search.SearchPage
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
import me.him188.ani.app.ui.main.SearchViewModel
import me.him188.ani.app.ui.subject.details.SubjectDetailsScene

@Composable
fun SearchScreen(
onNavigateBack: () -> Unit,
onNavigateToEpisodeDetails: (subjectId: Int, episodeId: Int) -> Unit,
modifier: Modifier = Modifier
) {
val vm = viewModel { SearchViewModel() }
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
SearchPage(
vm.searchPageState,
detailContent = {
val subjectDetailsState by vm.subjectDetailsStateLoader.state
.collectAsStateWithLifecycle(null)
val authState by vm.authState.collectAsStateWithLifecycle(AuthState.NotAuthed)
SubjectDetailsScene(
subjectDetailsState,
authState,
onPlay = { episodeId ->
val current = subjectDetailsState
if (current != null) {
onNavigateToEpisodeDetails(current.subjectId, episodeId)
}
},
onLoadErrorRetry = { vm.reloadCurrentSubjectDetails() },
navigationIcon = {
// 只有在单面板模式下才显示返回按钮
if (listDetailLayoutParameters.isSinglePane) {
BackNavigationIconButton(
onNavigateBack = {
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
listDetailNavigator.navigateBack()
}
},
)
}
},
)
},
modifier.fillMaxSize(),
onSelect = { index, item ->
vm.searchPageState.selectedItemIndex = index
vm.viewSubjectDetails(item)
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
}
},
navigator = listDetailNavigator,
navigationIcon = {
BackNavigationIconButton(onNavigateBack)
},
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ fun CommentColumn(
),
lazyStaggeredGridState = lazyStaggeredGridState,
contentPadding = contentPadding,
progressIndicator = null,
) {
item("spacer header") { Spacer(Modifier.height(1.dp)) }

Expand Down
Loading
Loading