Skip to content
Draft
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 @@ -9,7 +9,9 @@

package me.him188.ani.app.data.models.danmaku

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand Down Expand Up @@ -46,6 +48,21 @@ object DanmakuConfigSerializer : KSerializer<DanmakuConfig> {
val alpha: Float = DanmakuStyle.Default.alpha,
val strokeColor: ULong = DanmakuStyle.Default.strokeColor.value,
val strokeWidth: Float = DanmakuStyle.Default.strokeWidth,
/**
* `null` 表示不画阴影. 旧版本的配置里没有这个字段, 默认就是 `null`.
*/
val shadow: DanmakuShadowData? = null,
)

/**
* @see Shadow
*/
@Serializable
private class DanmakuShadowData(
val color: ULong,
val offsetX: Float,
val offsetY: Float,
val blurRadius: Float,
)

override val descriptor: SerialDescriptor = DanmakuConfigData.serializer().descriptor
Expand All @@ -60,6 +77,13 @@ object DanmakuConfigSerializer : KSerializer<DanmakuConfig> {
alpha = value.style.alpha,
strokeColor = Color(value.style.strokeColor),
strokeWidth = value.style.strokeWidth,
shadow = value.style.shadow?.let {
Shadow(
color = Color(it.color),
offset = Offset(it.offsetX, it.offsetY),
blurRadius = it.blurRadius,
)
},
),
speed = value.speed,
safeSeparation = value.safeSeparation.dp,
Expand All @@ -80,6 +104,14 @@ object DanmakuConfigSerializer : KSerializer<DanmakuConfig> {
alpha = value.style.alpha,
strokeColor = value.style.strokeColor.value,
strokeWidth = value.style.strokeWidth,
shadow = value.style.shadow?.let {
DanmakuShadowData(
color = it.color.value,
offsetX = it.offset.x,
offsetY = it.offset.y,
blurRadius = it.blurRadius,
)
},
),
speed = value.speed,
safeSeparation = value.safeSeparation.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import me.him188.ani.danmaku.api.ZhConversion
import me.him188.ani.utils.platform.annotations.SerializationOnly

/**
Expand All @@ -13,6 +14,18 @@ import me.him188.ani.utils.platform.annotations.SerializationOnly
@Serializable
data class DanmakuFilterConfig @SerializationOnly constructor(
val enableRegexFilter: Boolean = true,
/**
* 合并内容重复的弹幕. 见 `DanmakuMerger`.
*/
val enableMerge: Boolean = false,
/**
* 弹幕的简繁转换方式. 会同时作用于显示和过滤词匹配.
*/
val zhConversion: ZhConversion = ZhConversion.NONE,
/**
* 纯文本关键词屏蔽. 归一化之后按子串匹配, 比正则好写得多.
*/
val keywordBlocklist: List<String> = emptyList(),
@Suppress("PropertyName") @Transient val _placeholder: Int = 0
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ class DanmakuRepository(
list: List<DanmakuFetchResult>
) {
val entriesWithoutLocalSource = list
.filter { it.providerId != DanmakuProviderId.Local }
// Local: 本来就是从数据库读出来的.
// LocalFile: 用户导入的文件只在当前播放会话内有效, 不写进缓存.
.filter { it.providerId != DanmakuProviderId.Local && it.providerId != DanmakuProviderId.LocalFile }
.flatMap { it.toEntityList(subjectId, episodeId) }
logger.info { "saveToLocal, subjectId: $subjectId, episodeId: $episodeId, danmaku size: ${entriesWithoutLocalSource.size}" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
Expand All @@ -34,10 +35,12 @@ import me.him188.ani.app.domain.danmaku.DanmakuLoaderImpl
import me.him188.ani.app.domain.danmaku.DanmakuLoadingState
import me.him188.ani.app.domain.danmaku.DanmakuRepository
import me.him188.ani.app.domain.media.player.data.filenameOrNull
import me.him188.ani.app.domain.settings.GetDanmakuRegexFilterListFlowUseCase
import me.him188.ani.app.domain.settings.GetDanmakuFilterSpecFlowUseCase
import me.him188.ani.app.domain.settings.GetDanmakuPreprocessConfigFlowUseCase
import me.him188.ani.danmaku.api.DanmakuCollection
import me.him188.ani.danmaku.api.DanmakuEvent
import me.him188.ani.danmaku.api.DanmakuInfo
import me.him188.ani.danmaku.api.DanmakuPreprocessConfig
import me.him188.ani.danmaku.api.DanmakuServiceId
import me.him188.ani.danmaku.api.DanmakuSession
import me.him188.ani.danmaku.api.TimeBasedDanmakuSession
Expand Down Expand Up @@ -67,9 +70,11 @@ class EpisodeDanmakuLoader(
private val selectedMedia: Flow<Media?>,
private val bundleFlow: Flow<SubjectEpisodeInfoBundle>,
private val danmakuRepository: DanmakuRepository,
getDanmakuRegexFilterListFlowUseCase: GetDanmakuRegexFilterListFlowUseCase,
getDanmakuFilterSpecFlowUseCase: GetDanmakuFilterSpecFlowUseCase,
backgroundScope: CoroutineScope,
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(),
getDanmakuPreprocessConfigFlowUseCase: GetDanmakuPreprocessConfigFlowUseCase =
GetDanmakuPreprocessConfigFlowUseCase { flowOf(DanmakuPreprocessConfig.Default) },
) {
private val flowScope = backgroundScope

Expand Down Expand Up @@ -123,11 +128,16 @@ class EpisodeDanmakuLoader(
val configFlow = config.asStateFlow()


private val danmakuSessionFlow: Flow<DanmakuSession> = config.mapLatest { configMap ->
createDanmakuCollection(danmakuLoader.fetchResultFlow, configMap).at(
private val danmakuSessionFlow: Flow<DanmakuSession> = combine(
config,
getDanmakuPreprocessConfigFlowUseCase(),
) { configMap, preprocessConfig ->
configMap to preprocessConfig
}.mapLatest { (configMap, preprocessConfig) ->
createDanmakuCollection(danmakuLoader.fetchResultFlow, configMap, preprocessConfig).at(
progress = player.currentPositionMillis.map { it.milliseconds },
playbackSpeed = { player.features[PlaybackSpeed]?.value ?: 1f },
danmakuRegexFilterList = getDanmakuRegexFilterListFlowUseCase(),
filterSpec = getDanmakuFilterSpecFlowUseCase(),
)
}.shareIn(flowScope, started = sharingStarted, replay = 1)

Expand Down Expand Up @@ -181,7 +191,8 @@ class EpisodeDanmakuLoader(

private fun createDanmakuCollection(
danmakuListFlow: Flow<List<DanmakuFetchResult>?>,
config: Map<DanmakuServiceId, DanmakuOriginConfig>
config: Map<DanmakuServiceId, DanmakuOriginConfig>,
preprocessConfig: DanmakuPreprocessConfig,
): DanmakuCollection {
return TimeBasedDanmakuSession.create(
danmakuListFlow.map {
Expand All @@ -204,6 +215,7 @@ class EpisodeDanmakuLoader(
}
} ?: emptyList()
},
preprocessConfig = preprocessConfig,
)
}

Expand All @@ -228,6 +240,39 @@ class EpisodeDanmakuLoader(
danmakuLoader.overrideResults(provider, result)
}

/**
* 把用户从本地文件导入的弹幕挂到当前剧集上.
*
* 导入的弹幕会占用 [DanmakuProviderId.LocalFile] 这个独立的槽位, 因此和其他弹幕源一样
* 出现在弹幕源列表里, 支持开关和时间轴调整, 并且会走一遍完整的预处理流程. 它只在当前
* 播放会话内有效: 切换剧集时会被清空, 也不会写进本地缓存.
*
* 再次调用会替换掉上一次导入的弹幕.
*
* @param fileName 用于在弹幕源详情里显示导入的是哪个文件.
*/
fun setImportedDanmaku(fileName: String, list: List<DanmakuInfo>) {
logger.info { "Importing ${list.size} danmaku from file: $fileName" }
overrideResults(
DanmakuProviderId.LocalFile,
listOf(
DanmakuFetchResult(
providerId = DanmakuProviderId.LocalFile,
matchInfo = DanmakuMatchInfo(
serviceId = DanmakuServiceId.LocalFile,
count = list.size,
// 用户自己选的文件, 算精确匹配
method = DanmakuMatchMethod.Exact(
subjectTitle = fileName,
episodeTitle = fileName,
),
),
list = list,
),
),
)
}

/**
* 获取所有弹幕数据的流,用于弹幕列表显示
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import me.him188.ani.app.data.repository.player.DanmakuRegexFilterRepository
import me.him188.ani.app.data.repository.user.SettingsRepository
import me.him188.ani.app.domain.usecase.UseCase
import me.him188.ani.danmaku.api.DanmakuFilterSpec
import me.him188.ani.utils.logging.error
import me.him188.ani.utils.logging.logger
import org.koin.core.component.KoinComponent
Expand All @@ -25,32 +27,37 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.cancellation.CancellationException

/**
* Obtains the regex filters to be applied to danmaku collection, according to the user's settings.
* Obtains the filters to be applied to danmaku collection, according to the user's settings.
*/
fun interface GetDanmakuRegexFilterListFlowUseCase : UseCase {
operator fun invoke(): Flow<List<String>>
fun interface GetDanmakuFilterSpecFlowUseCase : UseCase {
operator fun invoke(): Flow<DanmakuFilterSpec>
}

class GetDanmakuRegexFilterListFlowUseCaseImpl(
class GetDanmakuFilterSpecFlowUseCaseImpl(
private val flowContext: CoroutineContext = Dispatchers.Default,
) : GetDanmakuRegexFilterListFlowUseCase, KoinComponent {
) : GetDanmakuFilterSpecFlowUseCase, KoinComponent {
private val settingsRepository: SettingsRepository by inject()
private val danmakuRegexFilterRepository: DanmakuRegexFilterRepository by inject()

override fun invoke(): Flow<List<String>> {
override fun invoke(): Flow<DanmakuFilterSpec> {
return combine(settingsRepository.danmakuFilterConfig.flow, danmakuRegexFilterRepository.flow) { config, list ->
if (!config.enableRegexFilter) emptyList()
else list.filter { it.enabled }.map { it.regex }
}.flowOn(flowContext)
DanmakuFilterSpec(
regexPatterns = if (!config.enableRegexFilter) emptyList()
else list.filter { it.enabled }.map { it.regex },
keywords = config.keywordBlocklist,
zhConversion = config.zhConversion,
)
}.distinctUntilChanged()
.flowOn(flowContext)
.catch { e ->
if (e !is CancellationException) {
logger.error(e) { "Failed to get danmaku regex filter list" }
logger.error(e) { "Failed to get danmaku filter spec" }
}
emit(listOf())
emit(DanmakuFilterSpec.Empty)
throw e
}
}


private val logger = logger<GetDanmakuRegexFilterListFlowUseCaseImpl>()
private val logger = logger<GetDanmakuFilterSpecFlowUseCaseImpl>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2024-2026 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.domain.settings

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import me.him188.ani.app.data.repository.user.SettingsRepository
import me.him188.ani.app.domain.usecase.UseCase
import me.him188.ani.danmaku.api.DanmakuPreprocessConfig
import me.him188.ani.utils.logging.error
import me.him188.ani.utils.logging.logger
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.cancellation.CancellationException

/**
* 根据用户设置, 得到加载弹幕时的预处理配置 (合并重复弹幕等).
*/
fun interface GetDanmakuPreprocessConfigFlowUseCase : UseCase {
operator fun invoke(): Flow<DanmakuPreprocessConfig>
}

class GetDanmakuPreprocessConfigFlowUseCaseImpl(
private val flowContext: CoroutineContext = Dispatchers.Default,
) : GetDanmakuPreprocessConfigFlowUseCase, KoinComponent {
private val settingsRepository: SettingsRepository by inject()

override fun invoke(): Flow<DanmakuPreprocessConfig> {
return settingsRepository.danmakuFilterConfig.flow
.map { config ->
DanmakuPreprocessConfig(
enableMerge = config.enableMerge,
zhConversion = config.zhConversion,
)
}
.distinctUntilChanged()
.flowOn(flowContext)
.catch { e ->
if (e !is CancellationException) {
logger.error(e) { "Failed to get danmaku preprocess config" }
}
emit(DanmakuPreprocessConfig.Default)
throw e
}
}

private val logger = logger<GetDanmakuPreprocessConfigFlowUseCaseImpl>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ import me.him188.ani.app.domain.mediasource.SetPreferredWebMediaSourceUseCase
import me.him188.ani.app.domain.mediasource.SetPreferredWebMediaSourceUseCaseImpl
import me.him188.ani.app.domain.mediasource.instance.GetMediaSourceInstancesUseCase
import me.him188.ani.app.domain.mediasource.instance.GetMediaSourceInstancesUseCaseImpl
import me.him188.ani.app.domain.settings.GetDanmakuRegexFilterListFlowUseCase
import me.him188.ani.app.domain.settings.GetDanmakuRegexFilterListFlowUseCaseImpl
import me.him188.ani.app.domain.settings.GetDanmakuFilterSpecFlowUseCase
import me.him188.ani.app.domain.settings.GetDanmakuFilterSpecFlowUseCaseImpl
import me.him188.ani.app.domain.settings.GetDanmakuPreprocessConfigFlowUseCase
import me.him188.ani.app.domain.settings.GetDanmakuPreprocessConfigFlowUseCaseImpl
import me.him188.ani.app.domain.settings.GetMediaSelectorSettingsFlowUseCase
import me.him188.ani.app.domain.settings.GetMediaSelectorSettingsFlowUseCaseImpl
import me.him188.ani.app.domain.settings.GetMediaSelectorSettingsUseCase
Expand All @@ -67,7 +69,8 @@ import org.koin.mp.KoinPlatform

fun KoinApplication.useCaseModules() = module {
single<GetEpisodeCollectionInfoFlowUseCase> { GetEpisodeCollectionInfoFlowUseCaseImpl() }
single<GetDanmakuRegexFilterListFlowUseCase> { GetDanmakuRegexFilterListFlowUseCaseImpl() }
single<GetDanmakuFilterSpecFlowUseCase> { GetDanmakuFilterSpecFlowUseCaseImpl() }
single<GetDanmakuPreprocessConfigFlowUseCase> { GetDanmakuPreprocessConfigFlowUseCaseImpl() }
single<MediaSelectorAutoSelectUseCase> { MediaSelectorAutoSelectUseCaseImpl() }
single<MediaSelectorEventSavePreferenceUseCase> { MediaSelectorEventSavePreferenceUseCaseImpl }
single<GetSubjectEpisodeInfoBundleFlowUseCase> { GetSubjectEpisodeInfoBundleFlowUseCaseImpl() }
Expand Down
Loading