Skip to content

Commit

Permalink
支持彩色弹幕, 可在弹幕设置中禁用
Browse files Browse the repository at this point in the history
  • Loading branch information
Him188 committed May 22, 2024
1 parent 532601e commit 3bd3881
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object DanmakuConfigSerializer : KSerializer<DanmakuConfig> {
val style: DanmakuStyleData = DanmakuStyleData(),
val speed: Float = DanmakuConfig.Default.speed,
val safeSeparation: Int = DanmakuConfig.Default.safeSeparation.value.toInt(),
val enableColor: Boolean = DanmakuConfig.Default.enableColor,
)

/**
Expand Down Expand Up @@ -45,6 +46,7 @@ object DanmakuConfigSerializer : KSerializer<DanmakuConfig> {
),
speed = data.speed,
safeSeparation = data.safeSeparation.dp,
enableColor = data.enableColor,
)
}

Expand All @@ -58,6 +60,7 @@ object DanmakuConfigSerializer : KSerializer<DanmakuConfig> {
),
speed = value.speed,
safeSeparation = value.safeSeparation.value.toInt(),
enableColor = value.enableColor,
)

return DanmakuConfigData.serializer().serialize(encoder, data)
Expand Down
2 changes: 1 addition & 1 deletion app/shared/pages/episode-play/common/EpisodeVideo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ internal fun EpisodeVideoImpl(
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200))
) {
DanmakuHost(danmakuHostState, Modifier.matchParentSize(), danmakuConfig())
DanmakuHost(danmakuHostState, Modifier.matchParentSize(), danmakuConfig)
}
},
gestureHost = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,210 +1,181 @@
package me.him188.ani.app.ui.subject.episode.video.settings

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.merge
import me.him188.ani.app.data.repositories.SettingsRepository
import me.him188.ani.app.ui.foundation.AbstractViewModel
import me.him188.ani.app.ui.foundation.launchInBackground
import me.him188.ani.app.ui.external.placeholder.placeholder
import me.him188.ani.app.ui.settings.SettingsTab
import me.him188.ani.app.ui.settings.SliderItem
import me.him188.ani.app.ui.settings.SwitchItem
import me.him188.ani.app.ui.settings.framework.AbstractSettingsViewModel
import me.him188.ani.app.ui.theme.aniDarkColorTheme
import me.him188.ani.danmaku.ui.DanmakuConfig
import me.him188.ani.danmaku.ui.DanmakuStyle
import me.him188.ani.utils.logging.info
import me.him188.ani.utils.logging.logger
import moe.tlaster.precompose.flow.collectAsStateWithLifecycle
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.seconds

@Stable
interface EpisodeVideoSettingsViewModel {
val danmakuConfig: Flow<DanmakuConfig>
val danmakuConfig: DanmakuConfig
val isLoading: Boolean

fun setDanmakuConfig(config: DanmakuConfig)
}


fun EpisodeVideoSettingsViewModel(): EpisodeVideoSettingsViewModel = EpisodeVideoSettingsViewModelImpl()

private class EpisodeVideoSettingsViewModelImpl : EpisodeVideoSettingsViewModel, AbstractViewModel(), KoinComponent {
private class EpisodeVideoSettingsViewModelImpl : EpisodeVideoSettingsViewModel, AbstractSettingsViewModel(),
KoinComponent {
private val settingsRepository by inject<SettingsRepository>()

private val danmakuConfigPersistent = settingsRepository.danmakuConfig.flow
private val danmakuConfigUpdate: MutableStateFlow<DanmakuConfig?> = MutableStateFlow(null)
override val danmakuConfig = merge(danmakuConfigPersistent, danmakuConfigUpdate).filterNotNull()
val danmakuConfigSettings by settings(
settingsRepository.danmakuConfig,
DanmakuConfig(_placeholder = -1)
)

init {
launchInBackground {
danmakuConfigUpdate.debounce(0.1.seconds).filterNotNull().collect {
logger.info { "Saving DanmakuConfig: $it" }
settingsRepository.danmakuConfig.set(it)
}
}
}
override val danmakuConfig: DanmakuConfig by danmakuConfigSettings
override val isLoading: Boolean get() = danmakuConfigSettings.loading

override fun setDanmakuConfig(config: DanmakuConfig) {
danmakuConfigUpdate.value = config
}

private companion object {
private val logger = logger(EpisodeVideoSettingsViewModelImpl::class)
danmakuConfigSettings.update(config)
}
}

private data class StepRange(
val range: ClosedFloatingPointRange<Float>,
val steps: Int,
)

//@Stable
//private fun generateRange(
// maxRange: ClosedFloatingPointRange<Float>,
// stepSize: Float,
// steps: Int
//): StepRange {
// return base - stepSize * steps / 2..base + stepSize * steps / 2
//}

@Composable
fun EpisodeVideoSettings(
vm: EpisodeVideoSettingsViewModel,
modifier: Modifier = Modifier,
) {
val danmakuConfig by vm.danmakuConfig.collectAsStateWithLifecycle(DanmakuConfig.Default)
return EpisodeVideoSettings(
danmakuConfig = danmakuConfig,
setDanmakuConfig = vm::setDanmakuConfig,
danmakuConfig = vm.danmakuConfig,
setDanmakuConfig = remember(vm) {
vm::setDanmakuConfig
},
isLoading = remember(vm) {
{ vm.isLoading }
},
modifier = modifier,
)
}

@Stable
private val LOADING_FALSE = { false }

@Composable
fun EpisodeVideoSettings(
danmakuConfig: DanmakuConfig,
setDanmakuConfig: (config: DanmakuConfig) -> Unit,
isLoading: () -> Boolean = LOADING_FALSE,
modifier: Modifier = Modifier,
) {
Column(modifier.verticalScroll(rememberScrollState())) {
Text(
"弹幕设置",
Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
style = MaterialTheme.typography.titleMedium
)

Column {
val isLoadingState by remember(isLoading) {
derivedStateOf(isLoading)
}
SettingsTab(modifier) {
Group(
useThinHeader = true,
title = {
Text("弹幕设置")
},
) {
val fontSize by remember(danmakuConfig) {
mutableFloatStateOf(danmakuConfig.style.fontSize.value / DanmakuStyle.Default.fontSize.value)
}
Text(
"弹幕字号: ${(fontSize * 100).roundToInt()}%",
Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.bodyMedium
)

Slider(
SliderItem(
value = fontSize,
onValueChange = {
// 故意每次改都更新, 可以即时预览
setDanmakuConfig(
danmakuConfig.copy(danmakuConfig.style.copy(fontSize = DanmakuStyle.Default.fontSize * it))
danmakuConfig.copy(style = danmakuConfig.style.copy(fontSize = DanmakuStyle.Default.fontSize * it))
)
},
valueRange = 0.50f..3f,
steps = ((3f - 0.50f) / 0.05f).toInt() - 1,
title = { Text("弹幕字号") },
valueLabel = { Text(remember(fontSize) { "${(fontSize * 100).roundToInt()}%" }) },
modifier = Modifier.placeholder(isLoadingState),
)
}

Column {
val alpha by remember(danmakuConfig) {
mutableFloatStateOf(danmakuConfig.style.alpha)
}
Text(
"不透明度: ${(alpha * 100).roundToInt()}%",
Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.bodyMedium
)

Slider(
SliderItem(
value = alpha,
onValueChange = {
setDanmakuConfig(
danmakuConfig.copy(danmakuConfig.style.copy(alpha = it))
danmakuConfig.copy(style = danmakuConfig.style.copy(alpha = it))
)
},
valueRange = 0f..1f,
steps = ((1f - 0f) / 0.05f).toInt() - 1,
title = { Text("不透明度") },
valueLabel = { Text(remember(alpha) { "${(alpha * 100).roundToInt()}%" }) },
modifier = Modifier.placeholder(isLoadingState),
)
}


Column {
var strokeMiterValue by remember(danmakuConfig) {
val strokeWidth by remember(danmakuConfig) {
mutableFloatStateOf(danmakuConfig.style.strokeWidth / DanmakuStyle.Default.strokeWidth)
}
Text(
"描边宽度: ${(strokeMiterValue * 100).roundToInt()}%",
Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
Slider(
value = strokeMiterValue,
onValueChange = { strokeMiterValue = it },
valueRange = 0f..2f,
steps = ((2f - 0f) / 0.1f).toInt() - 1,
onValueChangeFinished = {
SliderItem(
value = strokeWidth,
onValueChange = {
setDanmakuConfig(
danmakuConfig.copy(danmakuConfig.style.copy(strokeMidth = strokeMiterValue * DanmakuStyle.Default.strokeWidth))
danmakuConfig.copy(style = danmakuConfig.style.copy(strokeWidth = it * DanmakuStyle.Default.strokeWidth))
)
}
},
valueRange = 0f..2f,
steps = ((2f - 0f) / 0.1f).toInt() - 1,
title = { Text("描边宽度") },
valueLabel = { Text(remember(strokeWidth) { "${(strokeWidth * 100).roundToInt()}%" }) },
modifier = Modifier.placeholder(isLoadingState),
)
}

Column {
var speed by remember(danmakuConfig) {
mutableFloatStateOf(
danmakuConfig.speed / DanmakuConfig.Default.speed
)
}
Text(
"弹幕速度: ${(speed * 100).roundToInt()}%",
Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
Text(
"弹幕速度不会跟随视频倍速变化",
Modifier.padding(horizontal = 8.dp).padding(top = 4.dp),
style = MaterialTheme.typography.labelSmall
)
Slider(
SliderItem(
value = speed,
onValueChange = { speed = it },
valueRange = 0.2f..3f,
steps = ((3f - 0.2f) / 0.1f).toInt() - 1,
title = { Text("弹幕速度") },
description = { Text("弹幕速度不会跟随视频倍速变化") },
onValueChangeFinished = {
setDanmakuConfig(
danmakuConfig.copy(
speed = speed * DanmakuConfig.Default.speed
)
)
}
},
valueLabel = { Text(remember(speed) { "${(speed * 100).roundToInt()}%" }) },
modifier = Modifier.placeholder(isLoadingState),
)
var enableColor = remember(danmakuConfig) { danmakuConfig.enableColor }
SwitchItem(
enableColor,
onCheckedChange = {
enableColor = it
setDanmakuConfig(
danmakuConfig.copy(enableColor = it)
)
},
title = { Text("彩色弹幕") },
description = { Text("关闭后所有彩色弹幕都会显示为白色") },
modifier = Modifier.placeholder(isLoadingState),
)
}
}
Expand Down
14 changes: 7 additions & 7 deletions danmaku/ui/androidMain/DanmakuHost.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@ internal actual fun PreviewDanmakuHost() = ProvideCompositionLocalsForPreview {
Row {
DanmakuHost(
state,
Modifier.weight(1f),
config
)
Modifier.weight(1f)
) { config }
VerticalDivider()
EpisodeVideoSettings(
config,
{ config = it },
isLoading = { false },
Modifier.weight(1f)
)
}
Expand All @@ -113,13 +113,13 @@ internal actual fun PreviewDanmakuHost() = ProvideCompositionLocalsForPreview {
state,
Modifier
.weight(1f)
.fillMaxWidth(),
config
)
.fillMaxWidth()
) { config }
HorizontalDivider()
EpisodeVideoSettings(
config,
{ config = it },
isLoading = { false },
Modifier.weight(1f)
)
}
Expand All @@ -135,7 +135,7 @@ private fun PreviewDanmakuText() {
Surface(color = Color.White) {
DanmakuText(
DummyDanmakuState,
style = DanmakuStyle()
style = DanmakuStyle(),
)
}
}
Expand Down
Loading

0 comments on commit 3bd3881

Please sign in to comment.