Skip to content

Commit

Permalink
[new ui] 使用新版数据源选择器的外显 UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Him188 committed Mar 4, 2025
1 parent 49e669d commit 1ee64a0
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ import me.him188.ani.app.ui.foundation.layout.desktopTitleBar
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.isHeightCompact
import me.him188.ani.app.ui.foundation.layout.isSystemInFullscreen
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastExpanded
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.isWidthCompact
Expand Down Expand Up @@ -404,6 +403,7 @@ private fun EpisodeScreenTabletVeryWide(
val pageState by vm.pageState.collectAsStateWithLifecycle()
pageState?.let { page ->
EpisodeDetails(
page.mediaSelectorSummary,
vm.episodeDetailsState,
vm.episodeCarouselState,
vm.editableSubjectCollectionTypeState,
Expand Down Expand Up @@ -521,6 +521,7 @@ private fun EpisodeScreenContentPhone(
val pageState by vm.pageState.collectAsStateWithLifecycle()
pageState?.let { page ->
EpisodeDetails(
page.mediaSelectorSummary,
vm.episodeDetailsState,
vm.episodeCarouselState,
vm.editableSubjectCollectionTypeState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
Expand All @@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.him188.ani.app.data.models.episode.displayName
import me.him188.ani.app.data.models.episode.renderEpisodeEp
import me.him188.ani.app.data.models.preference.VideoScaffoldConfig
import me.him188.ani.app.data.models.subject.SubjectInfo
Expand Down Expand Up @@ -92,10 +94,13 @@ import me.him188.ani.app.ui.comment.CommentMapperContext.parseToUIComment
import me.him188.ani.app.ui.comment.CommentState
import me.him188.ani.app.ui.comment.EditCommentSticker
import me.him188.ani.app.ui.danmaku.UIDanmakuEvent
import me.him188.ani.app.ui.episode.PlayingEpisodeSummary
import me.him188.ani.app.ui.foundation.AbstractViewModel
import me.him188.ani.app.ui.foundation.HasBackgroundScope
import me.him188.ani.app.ui.foundation.launchInBackground
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.mediaselect.summary.MediaSelectorSummary
import me.him188.ani.app.ui.mediaselect.summary.QueriedSourcePresentation
import me.him188.ani.app.ui.settings.danmaku.DanmakuRegexFilterState
import me.him188.ani.app.ui.subject.AiringLabelState
import me.him188.ani.app.ui.subject.collection.components.EditableSubjectCollectionTypeState
Expand All @@ -120,6 +125,8 @@ import me.him188.ani.danmaku.api.DanmakuEvent
import me.him188.ani.danmaku.api.DanmakuPresentation
import me.him188.ani.danmaku.ui.DanmakuConfig
import me.him188.ani.datasources.api.PackedDate
import me.him188.ani.datasources.api.source.MediaSourceInfo
import me.him188.ani.datasources.api.source.MediaSourceKind
import me.him188.ani.datasources.api.topic.isDoneOrDropped
import me.him188.ani.utils.coroutines.flows.FlowRestarter
import me.him188.ani.utils.coroutines.flows.flowOfEmptyList
Expand All @@ -134,6 +141,7 @@ import org.openani.mediamp.MediampPlayer
import org.openani.mediamp.MediampPlayerFactory
import org.openani.mediamp.features.chapters
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds


@Stable
Expand All @@ -149,6 +157,8 @@ data class EpisodePageState(
val isLoading: Boolean = false,
val loadError: LoadError? = null,
val isPlaceholder: Boolean = false,
val playingEpisodeSummary: PlayingEpisodeSummary?, // null means placeholder TODO: should distinguish placeholder
val mediaSelectorSummary: MediaSelectorSummary,
)

/**
Expand Down Expand Up @@ -551,7 +561,61 @@ class EpisodeViewModel(
}
},
mediaSourceResultsFlow.map { MediaSourceResultListPresentation(it) },
) { authState, subjectEpisodeBundle, loadError, fetchSelect, danmakuStatistics, danmakuEnabled, danmakuConfig, mediaSelectorState, mediaSourceResultsPresentation ->
combineTransform(
episodeSession.fetchSelectFlow.map { fetchSelect ->
fetchSelect?.mediaSelector
},
mediaSourceResultsFlow,
settingsRepository.mediaSelectorSettings.flow,
) { mediaSelectorState, mediaSourceResultPresentations, mediaSelectorSettings ->
val queriedSourcesFlow = combine(
mediaSourceResultPresentations.map {
mediaSourceInfoProvider.getSourceInfoFlow(it.mediaSourceId)
},
) { mediaSourceInfos ->
mediaSourceInfos.filterNotNull()
.map {
it.toQueriedSourcePresentation()
}
}

if (mediaSelectorState == null) {
// still loading
emit(MediaSelectorSummary.AutoSelecting(listOf(), estimate = 10.seconds))
} else {
emitAll(
combine(queriedSourcesFlow, mediaSelectorState.selected) { queriedSources, selected ->
when {
selected != null -> {
MediaSelectorSummary.Selected(
mediaSourceInfoProvider.getSourceInfoFlow(selected.mediaSourceId).first()
?.toQueriedSourcePresentation() ?: QueriedSourcePresentation(
sourceName = selected.mediaSourceId,
sourceIconUrl = "",
),
selected.originalTitle,
)
}

mediaSelectorSettings.preferKind == MediaSourceKind.WEB -> {
MediaSelectorSummary.AutoSelecting(
queriedSources = queriedSources,
estimate = if (mediaSelectorSettings.fastSelectWebKind) mediaSelectorSettings.fastSelectWebKindAllowNonPreferredDelay
else 10.seconds,
)
}

else -> {
MediaSelectorSummary.RequiresManualSelection(
queriedSources = queriedSources,
)
}
}
},
)
}
},
) { authState, subjectEpisodeBundle, loadError, fetchSelect, danmakuStatistics, danmakuEnabled, danmakuConfig, mediaSelectorState, mediaSourceResultsPresentation, mediaSelectorSummary ->

val (subject, episode) = if (subjectEpisodeBundle == null) {
SubjectPresentation.Placeholder to EpisodePresentation.Placeholder
Expand All @@ -577,10 +641,30 @@ class EpisodeViewModel(
danmakuConfig = danmakuConfig,
isLoading = subjectEpisodeBundle == null,
loadError = loadError,
playingEpisodeSummary = if (subjectEpisodeBundle == null) {
null
} else {
PlayingEpisodeSummary(
episodeSort = subjectEpisodeBundle.episodeInfo.sort,
episodeName = subjectEpisodeBundle.episodeInfo.displayName,
subjectName = subjectEpisodeBundle.subjectInfo.displayName,
subjectTags = listOf(), // todo: tags, see figma
subjectCoverUrl = subjectEpisodeBundle.subjectInfo.imageLarge,
rating = subjectEpisodeBundle.subjectInfo.ratingInfo,
selfRatingInfo = subjectEpisodeBundle.subjectCollectionInfo.selfRatingInfo,
)
},
mediaSelectorSummary = mediaSelectorSummary,
)
}
}

private fun MediaSourceInfo.toQueriedSourcePresentation() =
QueriedSourcePresentation(
displayName,
iconUrl ?: "",
)

suspend fun switchEpisode(episodeId: Int) {
// 关闭弹窗
withContext(Dispatchers.Main.immediate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

package me.him188.ani.app.ui.subject.episode.details

import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -32,7 +31,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Dataset
import androidx.compose.material.icons.outlined.ExpandCircleDown
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -57,21 +55,19 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import me.him188.ani.app.data.models.episode.displayName
import me.him188.ani.app.data.models.subject.SubjectInfo
import me.him188.ani.app.domain.danmaku.DanmakuLoadingState
import me.him188.ani.app.domain.session.AuthState
import me.him188.ani.app.navigation.LocalNavigator
import me.him188.ani.app.ui.foundation.LocalPlatform
import me.him188.ani.app.ui.foundation.layout.desktopTitleBar
import me.him188.ani.app.ui.foundation.layout.desktopTitleBarPadding
import me.him188.ani.app.ui.foundation.layout.paddingIfNotEmpty
import me.him188.ani.app.ui.media.renderProperties
import me.him188.ani.app.ui.mediaselect.summary.MediaSelectorSummary
import me.him188.ani.app.ui.mediaselect.summary.MediaSelectorSummaryCard
import me.him188.ani.app.ui.subject.AiringLabel
import me.him188.ani.app.ui.subject.AiringLabelState
import me.him188.ani.app.ui.subject.collection.SubjectCollectionTypeSuggestions
Expand All @@ -83,18 +79,13 @@ import me.him188.ani.app.ui.subject.details.state.SubjectDetailsStateLoader
import me.him188.ani.app.ui.subject.episode.details.components.DanmakuMatchInfoGrid
import me.him188.ani.app.ui.subject.episode.details.components.DanmakuSourceCard
import me.him188.ani.app.ui.subject.episode.details.components.DanmakuSourceSettingsDropdown
import me.him188.ani.app.ui.subject.episode.details.components.EpisodeWatchStatusButton
import me.him188.ani.app.ui.subject.episode.details.components.PlayingEpisodeItem
import me.him188.ani.app.ui.subject.episode.details.components.PlayingEpisodeItemDefaults
import me.him188.ani.app.ui.subject.episode.mediaFetch.MediaSelectorState
import me.him188.ani.app.ui.subject.episode.mediaFetch.MediaSourceResultListPresentation
import me.him188.ani.app.ui.subject.episode.mediaFetch.MediaSourceResultPresentation
import me.him188.ani.app.ui.subject.episode.statistics.DanmakuMatchInfoSummaryRow
import me.him188.ani.app.ui.subject.episode.statistics.VideoLoadingSummary
import me.him188.ani.app.ui.subject.episode.statistics.VideoStatistics
import me.him188.ani.app.ui.subject.episode.video.DanmakuStatistics
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.datasources.api.topic.isDoneOrDropped
import me.him188.ani.datasources.api.unwrapCached
import me.him188.ani.utils.platform.isDesktop

Expand Down Expand Up @@ -122,6 +113,7 @@ class EpisodeDetailsState(
*/
@Composable
fun EpisodeDetails(
mediaSelectorSummary: MediaSelectorSummary,
state: EpisodeDetailsState,
episodeCarouselState: EpisodeCarouselState,
editableSubjectCollectionTypeState: EditableSubjectCollectionTypeState,
Expand Down Expand Up @@ -222,101 +214,31 @@ fun EpisodeDetails(
}
},
exposedEpisodeItem = { innerPadding ->
val originalMedia by remember {
derivedStateOf {
videoStatistics.playingMedia?.unwrapCached() // 显示原始来源
}
}
val mediaSelected by remember {
derivedStateOf {
originalMedia != null
}
}
episodeCarouselState.playingEpisode?.let { episode ->
Card(
Modifier.padding(innerPadding).animateContentSize(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
contentColor = contentColorFor(MaterialTheme.colorScheme.surfaceContainer),
),
var showMediaSelector by rememberSaveable { mutableStateOf(false) }
if (showMediaSelector) {
ModalBottomSheet(
{ showMediaSelector = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = LocalPlatform.current.isDesktop()),
modifier = Modifier.desktopTitleBarPadding().statusBarsPadding(),
contentWindowInsets = { BottomSheetDefaults.windowInsets.add(WindowInsets.desktopTitleBar()) },
) {
PlayingEpisodeItem(
episodeSort = { Text(episode.episodeInfo.sort.toString()) },
title = { Text(episode.episodeInfo.displayName) },
watchStatus = {
if (authState.isKnownLoggedIn) {
EpisodeWatchStatusButton(
episode.collectionType.isDoneOrDropped(),
onUnmark = {
episodeCarouselState.setCollectionType(
episode,
UnifiedCollectionType.NOT_COLLECTED,
)
},
onMarkAsDone = {
episodeCarouselState.setCollectionType(
episode,
UnifiedCollectionType.DONE,
)
},
enabled = !episodeCarouselState.isSettingCollectionType.collectAsStateWithLifecycle().value,
)
}
},
mediaSelected = mediaSelected,
mediaLabels = {
val mediaPropertiesText by remember {
derivedStateOf {
originalMedia?.renderProperties()
}
}
SelectionContainer { Text(mediaPropertiesText ?: "") }
},
filename = {
videoStatistics.playingFilename?.let {
SelectionContainer {
Text(it, maxLines = 3, overflow = TextOverflow.Ellipsis)
}
}
},
videoLoadingSummary = {
VideoLoadingSummary(videoStatistics.videoLoadingState)
},
mediaSource = {
var showMediaSelector by rememberSaveable { mutableStateOf(false) }
PlayingEpisodeItemDefaults.MediaSource(
media = originalMedia,
mediaSourceInfo = videoStatistics.playingMediaSourceInfo,
isLoading = videoStatistics.mediaSourceLoading,
onClick = { showMediaSelector = !showMediaSelector },
)
if (showMediaSelector) {
ModalBottomSheet(
{ showMediaSelector = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = LocalPlatform.current.isDesktop()),
modifier = Modifier.desktopTitleBarPadding().statusBarsPadding(),
contentWindowInsets = { BottomSheetDefaults.windowInsets.add(WindowInsets.desktopTitleBar()) },
) {
EpisodePlayMediaSelector(
mediaSelectorState,
mediaSourceResultListPresentation,
onDismissRequest = { showMediaSelector = false },
onRefresh = onRefreshMediaSources,
onRestartSource = onRestartSource,
onSelected = { showMediaSelector = false },
stickyHeaderBackgroundColor = BottomSheetDefaults.ContainerColor,
)
}
}
},
actions = {
val navigator = LocalNavigator.current
PlayingEpisodeItemDefaults.ActionShare(videoStatistics.playingMedia)
PlayingEpisodeItemDefaults.ActionCache({ navigator.navigateSubjectCaches(state.subjectId) })
},
EpisodePlayMediaSelector(
mediaSelectorState,
mediaSourceResultListPresentation,
onDismissRequest = { showMediaSelector = false },
onRefresh = onRefreshMediaSources,
onRestartSource = onRestartSource,
onSelected = { showMediaSelector = false },
stickyHeaderBackgroundColor = BottomSheetDefaults.ContainerColor,
)
}
}

MediaSelectorSummaryCard(
mediaSelectorSummary,
onClickManualSelect = { showMediaSelector = true },
Modifier.fillMaxWidth().padding(innerPadding),
)
},
danmakuStatisticsSummary = {
DanmakuMatchInfoSummaryRow(
Expand Down

0 comments on commit 1ee64a0

Please # to comment.