diff --git a/app/src/main/java/com/muedsa/tvbox/AppModule.kt b/app/src/main/java/com/muedsa/tvbox/AppModule.kt index a7142c4..3cf5ed4 100644 --- a/app/src/main/java/com/muedsa/tvbox/AppModule.kt +++ b/app/src/main/java/com/muedsa/tvbox/AppModule.kt @@ -2,18 +2,22 @@ package com.muedsa.tvbox import android.content.Context import androidx.room.Room +import com.muedsa.tvbox.danmaku.DanmakuService +import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayApiService +import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayAuthInterceptor +import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayDanmakuProvider import com.muedsa.tvbox.room.AppDatabase -import com.muedsa.tvbox.service.DanDanPlayApiService -import com.muedsa.tvbox.service.DanDanPlayAuthInterceptor import com.muedsa.tvbox.store.DataStoreRepo import com.muedsa.tvbox.tool.createJsonRetrofit import com.muedsa.tvbox.tool.createOkHttpClient import com.muedsa.util.AppUtil +import com.muedsa.util.OkHttpCacheInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import okhttp3.Cache import javax.inject.Singleton @Module @@ -44,12 +48,37 @@ internal object AppModule { @Provides @Singleton - fun provideDanDanPlayApiService(@ApplicationContext context: Context): DanDanPlayApiService = - createJsonRetrofit( - baseUrl = "https://api.dandanplay.net/api/", - service = DanDanPlayApiService::class.java, - okHttpClient = createOkHttpClient(debug = AppUtil.debuggable(context)) { - addInterceptor(DanDanPlayAuthInterceptor()) - } + fun provideOkhttpCache(@ApplicationContext context: Context) = Cache( + directory = context.cacheDir.resolve("http_cache"), + maxSize = 50 * 1024 * 1024, + ) + + @Provides + @Singleton + fun provideDanDanPlayDanmakuProvider( + @ApplicationContext context: Context, + okHttpCache: Cache, + ) = DanDanPlayDanmakuProvider( + danDanPlayApiService = createJsonRetrofit( + baseUrl = "https://api.dandanplay.net/api/", + service = DanDanPlayApiService::class.java, + okHttpClient = createOkHttpClient(debug = AppUtil.debuggable(context)) { + cache(okHttpCache) + addInterceptor(DanDanPlayAuthInterceptor()) + addNetworkInterceptor(OkHttpCacheInterceptor()) + } + ) ) + + @Provides + @Singleton + fun provideDanmakuService( + danDanPlayDanmakuProvider: DanDanPlayDanmakuProvider, + ) = DanmakuService().also { + if (BuildConfig.DANDANPLAY_APP_ID.isNotEmpty() + && BuildConfig.DANDANPLAY_APP_SECRET.isNotEmpty() + ) { + it.register(danDanPlayDanmakuProvider) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuProvider.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuProvider.kt new file mode 100644 index 0000000..7b39e62 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuProvider.kt @@ -0,0 +1,16 @@ +package com.muedsa.tvbox.danmaku + +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.muedsa.tvbox.model.DanmakuEpisode +import com.muedsa.tvbox.model.DanmakuMedia + +interface DanmakuProvider { + + val name: String + + suspend fun searchMedia(keyword: String): List + + suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? + + suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List +} \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuService.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuService.kt new file mode 100644 index 0000000..bec033f --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuService.kt @@ -0,0 +1,30 @@ +package com.muedsa.tvbox.danmaku + +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.muedsa.tvbox.model.DanmakuEpisode +import com.muedsa.tvbox.model.DanmakuMedia + +class DanmakuService { + + private val providers: MutableMap = mutableMapOf() + + fun register(provider: DanmakuProvider) { + providers[provider.name] = provider + } + + fun getProviders(): Set { + return providers.keys + } + + suspend fun searchMedia(provider: String, keyword: String): List { + return providers[provider]?.searchMedia(keyword) ?: emptyList() + } + + suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? { + return providers[media.provider]?.getMediaEpisodes(media) + } + + suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List { + return providers[episode.provider]?.getEpisodeDanmakuList(episode) ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/service/DanDanPlayApiService.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayApiService.kt similarity index 60% rename from app/src/main/java/com/muedsa/tvbox/service/DanDanPlayApiService.kt rename to app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayApiService.kt index b6cab7f..284ccef 100644 --- a/app/src/main/java/com/muedsa/tvbox/service/DanDanPlayApiService.kt +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayApiService.kt @@ -1,10 +1,12 @@ -package com.muedsa.tvbox.service +package com.muedsa.tvbox.danmaku.dandanplay import com.muedsa.tvbox.model.dandanplay.BangumiDetailsResp import com.muedsa.tvbox.model.dandanplay.BangumiSearch import com.muedsa.tvbox.model.dandanplay.BangumiSearchResp import com.muedsa.tvbox.model.dandanplay.EpisodeComments +import com.muedsa.util.OkHttpCacheInterceptor import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Path import retrofit2.http.Query @@ -13,12 +15,14 @@ interface DanDanPlayApiService { @GET("v2/search/anime") suspend fun searchAnime( @Query("keyword") keyword: String, - @Query("type") type: String = "" + @Query("type") type: String = "", + @Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=3600", ): BangumiSearchResp @GET("v2/bangumi/{animeId}") suspend fun getAnime( - @Path("animeId") animeId: Int + @Path("animeId") animeId: Int, + @Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=21600", ): BangumiDetailsResp @GET("v2/comment/{episodeId}") @@ -26,7 +30,8 @@ interface DanDanPlayApiService { @Path("episodeId") episodeId: Long, @Query("from") from: Int = 0, @Query("withRelated") withRelated: Boolean = false, - @Query("chConvert") chConvert: Int = 0 + @Query("chConvert") chConvert: Int = 0, + @Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=21600", ): EpisodeComments } \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/service/DanDanPlayAuthInterceptor.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayAuthInterceptor.kt similarity index 95% rename from app/src/main/java/com/muedsa/tvbox/service/DanDanPlayAuthInterceptor.kt rename to app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayAuthInterceptor.kt index 3b67e07..4282053 100644 --- a/app/src/main/java/com/muedsa/tvbox/service/DanDanPlayAuthInterceptor.kt +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayAuthInterceptor.kt @@ -1,4 +1,4 @@ -package com.muedsa.tvbox.service +package com.muedsa.tvbox.danmaku.dandanplay import com.muedsa.tvbox.BuildConfig import com.muedsa.tvbox.tool.encodeBase64 diff --git a/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayDanmakuProvider.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayDanmakuProvider.kt new file mode 100644 index 0000000..c5d8010 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/dandanplay/DanDanPlayDanmakuProvider.kt @@ -0,0 +1,91 @@ +package com.muedsa.tvbox.danmaku.dandanplay + +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.muedsa.tvbox.danmaku.DanmakuProvider +import com.muedsa.tvbox.model.DanmakuEpisode +import com.muedsa.tvbox.model.DanmakuMedia +import timber.log.Timber + +class DanDanPlayDanmakuProvider( + val danDanPlayApiService: DanDanPlayApiService, +) : DanmakuProvider { + + override val name: String = "弹弹Play" + + override suspend fun searchMedia(keyword: String): List { + val resp = danDanPlayApiService.searchAnime(keyword) + return if (resp.errorCode == 0) { + resp.animes?.map { + DanmakuMedia( + provider = name, + mediaId = it.animeId.toString(), + mediaName = it.animeTitle, + publishDate = it.startOnlyDate, + rating = it.rating.toString(), + episodes = emptyList(), + ) + } ?: emptyList() + } else { + Timber.w("danDanPlayApiService.searchAnime(${keyword}) ${resp.errorMessage}") + emptyList() + } + } + + override suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? { + val resp = danDanPlayApiService.getAnime(media.mediaId.toInt()) + return if (resp.errorCode == 0) { + resp.bangumi?.let { + DanmakuMedia( + provider = name, + mediaId = it.animeId.toString(), + mediaName = it.animeTitle, + publishDate = media.publishDate, + rating = it.rating.toString(), + episodes = it.episodes.map { ep -> + DanmakuEpisode( + provider = name, + mediaId = it.animeId.toString(), + mediaName = it.animeTitle, + episodeId = ep.episodeId.toString(), + episodeName = ep.episodeTitle, + ) + }, + ) + } + } else { + Timber.d("danDanPlayApiService.getAnime(${media.mediaId}) ${resp.errorMessage}") + null + } + } + + override suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List { + return danDanPlayApiService.getComment( + episodeId = episode.episodeId.toLong(), + from = 0, + withRelated = true, + chConvert = 1 + ).comments.map { + val propArr = it.p.split(",") + val pos = (propArr[0].toFloat() * 1000).toLong() + val mode = if (propArr[1] == "1") + DanmakuItemData.DANMAKU_MODE_ROLLING + else if (propArr[1] == "4") + DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM + else if (propArr[1] == "5") + DanmakuItemData.DANMAKU_MODE_CENTER_TOP + else + DanmakuItemData.DANMAKU_MODE_ROLLING + val colorInt = propArr[2].toInt() + DanmakuItemData( + danmakuId = it.cid, + position = pos, + content = it.m, + mode = mode, + textSize = 25, + textColor = colorInt, + score = 9, + danmakuStyle = DanmakuItemData.DANMAKU_STYLE_NONE + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/model/DanmakuEpisode.kt b/app/src/main/java/com/muedsa/tvbox/model/DanmakuEpisode.kt new file mode 100644 index 0000000..5e2374f --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/DanmakuEpisode.kt @@ -0,0 +1,12 @@ +package com.muedsa.tvbox.model + +import kotlinx.serialization.Serializable + +@Serializable +class DanmakuEpisode( + val provider: String, + val mediaId: String, + val mediaName: String, + val episodeId: String, + val episodeName: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/model/DanmakuMedia.kt b/app/src/main/java/com/muedsa/tvbox/model/DanmakuMedia.kt new file mode 100644 index 0000000..7592ec5 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/DanmakuMedia.kt @@ -0,0 +1,10 @@ +package com.muedsa.tvbox.model + +class DanmakuMedia( + val provider: String, + val mediaId: String, + val mediaName: String, + val publishDate: String? = null, + val rating: String? = null, + val episodes: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/screens/NavigationItems.kt b/app/src/main/java/com/muedsa/tvbox/screens/NavigationItems.kt index 1c4260d..63bb6a3 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/NavigationItems.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/NavigationItems.kt @@ -25,7 +25,7 @@ sealed interface NavigationItems { val pluginPackage: String, val mediaId: String, val episodeId: String, - val danEpisodeId: Long = -1, + val danmakuEpisodeJson: String? = null, val disableEpisodeProgression: Boolean, val enableCustomDanmakuList: Boolean, val enableCustomDanmakuFlow: Boolean, diff --git a/app/src/main/java/com/muedsa/tvbox/screens/detail/AnimeDanmakuSelectorWidget.kt b/app/src/main/java/com/muedsa/tvbox/screens/detail/DanmakuMediaSelectorWidget.kt similarity index 90% rename from app/src/main/java/com/muedsa/tvbox/screens/detail/AnimeDanmakuSelectorWidget.kt rename to app/src/main/java/com/muedsa/tvbox/screens/detail/DanmakuMediaSelectorWidget.kt index c57e0f2..fe2c63f 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/detail/AnimeDanmakuSelectorWidget.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/detail/DanmakuMediaSelectorWidget.kt @@ -40,15 +40,15 @@ import com.muedsa.compose.tv.widget.NoBackground import com.muedsa.compose.tv.widget.TwoSideWideButton @Composable -fun AnimeDanmakuSelectBtnWidget( +fun DanmakuMediaSelectorWidget( enabledDanmakuState: MutableState = remember { mutableStateOf(false) }, - mediaDetailScreenViewModel: MediaDetailScreenViewModel + mediaDetailScreenViewModel: MediaDetailScreenViewModel, ) { val drawerController = useLocalRightSideDrawerController() OutlinedIconButton(onClick = { drawerController.pop { - DanmakuSelectorSideWidget( + DanmakuMediaSelectorSideWidget( enabledDanmakuState = enabledDanmakuState, mediaDetailScreenViewModel = mediaDetailScreenViewModel ) @@ -56,13 +56,13 @@ fun AnimeDanmakuSelectBtnWidget( }) { Icon( imageVector = Icons.Outlined.Edit, - contentDescription = "修改弹弹Play匹配剧集" + contentDescription = "修改弹幕匹配剧集" ) } } @Composable -fun DanmakuSelectorSideWidget( +fun DanmakuMediaSelectorSideWidget( enabledDanmakuState: MutableState = remember { mutableStateOf(false) }, mediaDetailScreenViewModel: MediaDetailScreenViewModel ) { @@ -75,10 +75,10 @@ fun DanmakuSelectorSideWidget( return } val mediaDetail = (ui as MediaDetailScreenUiState.Ready).mediaDetail - val danBangumiList = (ui as MediaDetailScreenUiState.Ready).danBangumiList ?: emptyList() - val danBangumiInfo = (ui as MediaDetailScreenUiState.Ready).danBangumiInfo + val danmakuMediaList = (ui as MediaDetailScreenUiState.Ready).danmakuMediaList ?: emptyList() + val danmakuMediaInfo = (ui as MediaDetailScreenUiState.Ready).danmakuMediaInfo - var searchTitle by remember { mutableStateOf(danBangumiInfo?.animeTitle ?: mediaDetail.title) } + var searchTitle by remember { mutableStateOf(danmakuMediaInfo?.mediaName ?: mediaDetail.title) } val splitTitles = mediaDetail.title.split("\\s+".toRegex()) @@ -115,19 +115,19 @@ fun DanmakuSelectorSideWidget( ) } } - items(items = danBangumiList, key = { it.animeId }) { + items(items = danmakuMediaList, key = { it.mediaName }) { val interactionSource = remember { MutableInteractionSource() } TwoSideWideButton( title = { Text( modifier = Modifier.basicMarquee(), - text = "${it.animeTitle} - ${it.typeDescription} - ${it.startOnlyDate}" + text = "${it.mediaName} - ${it.publishDate}" ) }, onClick = { drawerController.close() enabledDanmakuState.value = true - mediaDetailScreenViewModel.changeDanBangumi(it) + mediaDetailScreenViewModel.changeDanmakuMedia(it) }, interactionSource = interactionSource, background = { @@ -138,7 +138,7 @@ fun DanmakuSelectorSideWidget( ) { RadioButton( selected = enabledDanmakuState.value - && danBangumiInfo?.animeId == it.animeId, + && danmakuMediaInfo?.mediaId == it.mediaId, onClick = { }, interactionSource = interactionSource ) @@ -199,7 +199,7 @@ fun DanmakuSelectorSideWidget( ) { Button( onClick = { - mediaDetailScreenViewModel.searchDanBangumi(searchTitle) + mediaDetailScreenViewModel.searchDanmakuMedia(searchTitle) isSelectorTab = true } ) { @@ -221,7 +221,7 @@ fun DanmakuSelectorSideWidget( modifier = Modifier.padding(top = 10.dp), onClick = { searchTitle = it - mediaDetailScreenViewModel.searchDanBangumi(searchTitle) + mediaDetailScreenViewModel.searchDanmakuMedia(searchTitle) isSelectorTab = true }, title = { diff --git a/app/src/main/java/com/muedsa/tvbox/screens/detail/DanmakuProviderSelectorWidget.kt b/app/src/main/java/com/muedsa/tvbox/screens/detail/DanmakuProviderSelectorWidget.kt new file mode 100644 index 0000000..e30c7aa --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/screens/detail/DanmakuProviderSelectorWidget.kt @@ -0,0 +1,115 @@ +package com.muedsa.tvbox.screens.detail + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.RadioButton +import androidx.tv.material3.Text +import androidx.tv.material3.WideButtonDefaults +import com.muedsa.compose.tv.conditional +import com.muedsa.compose.tv.useLocalRightSideDrawerController +import com.muedsa.compose.tv.widget.NoBackground +import com.muedsa.compose.tv.widget.TwoSideWideButton + +@Composable +fun DanmakuProviderSelectorWidget( + mediaDetailScreenViewModel: MediaDetailScreenViewModel, +) { + val drawerController = useLocalRightSideDrawerController() + val selectedDanmakuProvider by mediaDetailScreenViewModel.selectedDanmakuProviderFlow + .collectAsStateWithLifecycle() + var hasFocus by remember { mutableStateOf(false) } + + Text( + modifier = Modifier + .conditional(hasFocus) { + border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(6.dp), + ) + } + .padding(4.dp) + .onFocusChanged { + hasFocus = it.hasFocus + } + .clickable(onClick = { + drawerController.pop { + DanmakuProviderSelectorSideWidget( + mediaDetailScreenViewModel = mediaDetailScreenViewModel, + ) + } + }), + text = selectedDanmakuProvider ?: "--", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleMedium + ) +} + +@Composable +fun DanmakuProviderSelectorSideWidget( + mediaDetailScreenViewModel: MediaDetailScreenViewModel, +) { + val drawerController = useLocalRightSideDrawerController() + val danmakuProviders = remember { mediaDetailScreenViewModel.getDanmakuProviders().toList() } + val selectedDanmakuProvider by mediaDetailScreenViewModel.selectedDanmakuProviderFlow + .collectAsStateWithLifecycle() + + Column { + Text( + modifier = Modifier + .padding(start = 8.dp, end = 15.dp), + text = "选择弹幕提供者", + style = MaterialTheme.typography.titleLarge + ) + + LazyColumn( + contentPadding = PaddingValues(vertical = 20.dp) + ) { + items(items = danmakuProviders, key = { it }) { + val interactionSource = remember { MutableInteractionSource() } + TwoSideWideButton( + title = { + Text( + modifier = Modifier.basicMarquee(), + text = it + ) + }, + onClick = { + drawerController.close() + mediaDetailScreenViewModel.changeDanmakuProvider(it) + }, + interactionSource = interactionSource, + background = { + WideButtonDefaults.NoBackground( + interactionSource = interactionSource + ) + } + ) { + RadioButton( + selected = selectedDanmakuProvider == it, + onClick = { }, + interactionSource = interactionSource + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/screens/detail/EpisodeListWidget.kt b/app/src/main/java/com/muedsa/tvbox/screens/detail/EpisodeListWidget.kt index 4ba86a7..7a1cfab 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/detail/EpisodeListWidget.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/detail/EpisodeListWidget.kt @@ -33,7 +33,7 @@ import androidx.tv.material3.WideButton import com.muedsa.compose.tv.focusOnMount import com.muedsa.compose.tv.theme.CommonRowCardPadding import com.muedsa.tvbox.api.data.MediaEpisode -import com.muedsa.tvbox.model.dandanplay.BangumiEpisode +import com.muedsa.tvbox.model.DanmakuEpisode import com.muedsa.tvbox.room.model.EpisodeProgressModel import kotlin.math.max @@ -45,26 +45,29 @@ val WideButtonCornerRadius = 12.dp fun EpisodeListWidget( modifier: Modifier = Modifier, episodeList: List, - danEpisodeList: List, + danmakuEpisodeList: List, episodeProgressMap: Map, - episodeRelationMap: Map, + episodeRelationMap: Map, enabled: Boolean = true, - onEpisodeClick: (MediaEpisode, BangumiEpisode?) -> Unit = { _, _ -> }, - onChangeEpisodeRelation: (List>) -> Unit = {} + onEpisodeClick: (MediaEpisode, DanmakuEpisode?) -> Unit = { _, _ -> }, + onChangeEpisodeRelation: (List>) -> Unit = {} ) { val episodeListChunks = episodeList.chunked(EpisodePageSize) - val danEpisodeListChunks = danEpisodeList.chunked(EpisodePageSize) + val danmakuEpisodeListChunks = danmakuEpisodeList.chunked(EpisodePageSize) - var changeDanEpisodeMode by remember { mutableStateOf(false) } + var changeDanmakuEpisodeMode by remember { mutableStateOf(false) } var selectedEpisodeIndex by remember { mutableIntStateOf(0) } var selectedEpisode by remember { mutableStateOf(episodeList[0]) } - BackHandler(enabled = changeDanEpisodeMode) { - changeDanEpisodeMode = false + BackHandler(enabled = changeDanmakuEpisodeMode) { + changeDanmakuEpisodeMode = false } - Crossfade(targetState = changeDanEpisodeMode, label = "changeDanEpisodeCrossFade") { + Crossfade( + targetState = changeDanmakuEpisodeMode, + label = "changeDanmakuEpisodeCrossFade", + ) { Column(modifier = modifier) { if (!it) { episodeListChunks.forEachIndexed { chunkIndex, currentPartEpisodeList -> @@ -84,7 +87,7 @@ fun EpisodeListWidget( style = MaterialTheme.typography.titleMedium, maxLines = 1 ) - if (chunkIndex == 0 && danEpisodeList.isNotEmpty()) { + if (chunkIndex == 0 && danmakuEpisodeList.isNotEmpty()) { Spacer(modifier = Modifier.width(CommonRowCardPadding)) Text( text = "长按更改匹配的弹幕剧集", @@ -144,35 +147,36 @@ fun EpisodeListWidget( Text(text = episode.name, overflow = TextOverflow.Ellipsis) }, subtitle = { - val danEpisode = getDanEpisode( + val danmakuEpisode = getDanmakuEpisode( episode = episode, episodeIndex = episodePartIndex + chunkIndex * EpisodePageSize, - danEpisodeList = danEpisodeList, - episodeRelationMap = episodeRelationMap + danEpisodeList = danmakuEpisodeList, + episodeRelationMap = episodeRelationMap, ) - if (danEpisode != null) { + if (danmakuEpisode != null) { Text( - text = danEpisode.episodeTitle, - overflow = TextOverflow.Ellipsis + text = danmakuEpisode.episodeName, + overflow = TextOverflow.Ellipsis, ) } }, onClick = { onEpisodeClick( - episode, getDanEpisode( + episode, + getDanmakuEpisode( episode = episode, episodeIndex = episodePartIndex + chunkIndex * EpisodePageSize, - danEpisodeList = danEpisodeList, - episodeRelationMap = episodeRelationMap + danEpisodeList = danmakuEpisodeList, + episodeRelationMap = episodeRelationMap, ) ) }, onLongClick = { - if (danEpisodeList.isNotEmpty()) { + if (danmakuEpisodeList.isNotEmpty()) { val index = episodePartIndex + chunkIndex * EpisodePageSize selectedEpisodeIndex = index selectedEpisode = episodeList[index] - changeDanEpisodeMode = true + changeDanmakuEpisodeMode = true } } ) @@ -212,9 +216,9 @@ fun EpisodeListWidget( ) } - danEpisodeListChunks.forEachIndexed { danChunkIndex, danEpisodeList -> + danmakuEpisodeListChunks.forEachIndexed { danChunkIndex, danmakuEpisodeList -> val episodePartStartNo = 1 + danChunkIndex * EpisodePageSize - val episodePartEndNo = episodePartStartNo - 1 + danEpisodeList.size + val episodePartEndNo = episodePartStartNo - 1 + danmakuEpisodeList.size Row( modifier = Modifier.padding( start = CommonRowCardPadding, @@ -241,14 +245,14 @@ fun EpisodeListWidget( LazyRow { itemsIndexed( - items = danEpisodeList, - ) { danEpisodePartIndex, danEpisode -> + items = danmakuEpisodeList, + ) { danEpisodePartIndex, danmakuEpisode -> val interactionSource = remember { MutableInteractionSource() } WideButton( modifier = Modifier.padding(end = 12.dp), title = { Text( - text = danEpisode.episodeTitle, + text = danmakuEpisode.episodeName, overflow = TextOverflow.Ellipsis ) }, @@ -263,22 +267,22 @@ fun EpisodeListWidget( } }, onClick = { - onChangeEpisodeRelation(listOf(selectedEpisode.id to danEpisode)) - changeDanEpisodeMode = false + onChangeEpisodeRelation(listOf(selectedEpisode.id to danmakuEpisode)) + changeDanmakuEpisodeMode = false }, onLongClick = { onChangeEpisodeRelation(buildList { var danEpisodePos = danEpisodePartIndex + danChunkIndex * EpisodePageSize for (episodePos in selectedEpisodeIndex..= danEpisodeList.size) { + if (danEpisodePos >= danmakuEpisodeList.size) { break } - add(episodeList[episodePos].id to danEpisodeList[danEpisodePos]) + add(episodeList[episodePos].id to danmakuEpisodeList[danEpisodePos]) danEpisodePos++ } }) - changeDanEpisodeMode = false + changeDanmakuEpisodeMode = false }, interactionSource = interactionSource ) @@ -294,13 +298,13 @@ fun EpisodeListWidget( } } -fun getDanEpisode( +fun getDanmakuEpisode( episode: MediaEpisode, episodeIndex: Int, - danEpisodeList: List, - episodeRelationMap: Map -): BangumiEpisode? { - var danEpisode: BangumiEpisode? = null + danEpisodeList: List, + episodeRelationMap: Map +): DanmakuEpisode? { + var danEpisode: DanmakuEpisode? = null if (danEpisodeList.isNotEmpty()) { val episodeId = episodeRelationMap[episode.id] diff --git a/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreen.kt b/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreen.kt index df2e569..531261a 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreen.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreen.kt @@ -32,8 +32,8 @@ fun MediaDetailScreen( mediaDetail = s.mediaDetail, favorite = s.favorite, progressMap = s.progressMap, - danBangumiList = s.danBangumiList, - danBangumiInfo = s.danBangumiInfo, + danmakuMediaList = s.danmakuMediaList, + danmakuMediaInfo = s.danmakuMediaInfo, mediaDetailScreenViewModel = mediaDetailScreenViewModel ) } diff --git a/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreenViewModel.kt b/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreenViewModel.kt index 3416ef0..f96ca67 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreenViewModel.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailScreenViewModel.kt @@ -2,15 +2,14 @@ package com.muedsa.tvbox.screens.detail import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.muedsa.tvbox.BuildConfig import com.muedsa.tvbox.api.data.MediaDetail import com.muedsa.tvbox.api.data.MediaEpisode import com.muedsa.tvbox.api.data.MediaHttpSource import com.muedsa.tvbox.api.data.MediaPlaySource import com.muedsa.tvbox.api.data.SavedMediaCard import com.muedsa.tvbox.api.plugin.PluginOptions -import com.muedsa.tvbox.model.dandanplay.BangumiInfo -import com.muedsa.tvbox.model.dandanplay.BangumiSearch +import com.muedsa.tvbox.danmaku.DanmakuService +import com.muedsa.tvbox.model.DanmakuMedia import com.muedsa.tvbox.plugin.PluginInfo import com.muedsa.tvbox.plugin.PluginManager import com.muedsa.tvbox.room.dao.EpisodeProgressDao @@ -18,7 +17,6 @@ import com.muedsa.tvbox.room.dao.FavoriteMediaDao import com.muedsa.tvbox.room.model.EpisodeProgressModel import com.muedsa.tvbox.room.model.FavoriteMediaModel import com.muedsa.tvbox.screens.NavigationItems -import com.muedsa.tvbox.service.DanDanPlayApiService import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -36,7 +34,7 @@ import javax.inject.Inject @HiltViewModel class MediaDetailScreenViewModel @Inject constructor( - private val danDanPlayApiService: DanDanPlayApiService, + private val danmakuService: DanmakuService, private val favoriteMediaDao: FavoriteMediaDao, private val episodeProgressDao: EpisodeProgressDao ) : ViewModel() { @@ -89,29 +87,33 @@ class MediaDetailScreenViewModel @Inject constructor( } } - private val _banBangumiSearchQueryFlow = MutableStateFlow(null) + private val _selectedDanmakuProviderFlow = MutableStateFlow( + danmakuService.getProviders().firstOrNull() + ) + val selectedDanmakuProviderFlow: StateFlow = _selectedDanmakuProviderFlow - private val _danBangumiListFlow = - combine(_mediaDetailFlow, _banBangumiSearchQueryFlow) { wrapper, searchQuery -> + private val _danmakuMediaSearchQueryFlow = MutableStateFlow(null) + + private val _danmakuMediaListFlow = + combine(_mediaDetailFlow, _selectedDanmakuProviderFlow, _danmakuMediaSearchQueryFlow) { wrapper, danmakuProvider, searchQuery -> if (wrapper.data != null - && !BuildConfig.DANDANPLAY_APP_ID.isEmpty() - && !BuildConfig.DANDANPLAY_APP_SECRET.isEmpty() && wrapper.data.second.enableDanDanPlaySearch && !wrapper.data.third.enableCustomDanmakuList && !wrapper.data.third.enableCustomDanmakuFlow ) { - try { - val resp = - danDanPlayApiService.searchAnime(searchQuery ?: wrapper.data.third.title) - val list = if (resp.errorCode == 0) { - resp.animes ?: emptyList() - } else { - Timber.d("danDanPlayApiService.searchAnime(${wrapper.data.third.title}) ${resp.errorMessage}") - emptyList() + val dp = danmakuProvider ?: danmakuService.getProviders().firstOrNull() + if (dp != null) { + try { + val list = danmakuService.searchMedia( + provider = dp, + keyword = searchQuery ?: wrapper.data.third.title + ) + Pair(wrapper.data, list) + } catch (throwable: Throwable) { + Timber.e(throwable) + Pair(wrapper.data, emptyList()) } - Pair(wrapper.data, list) - } catch (throwable: Throwable) { - Timber.e(throwable) + } else { Pair(wrapper.data, emptyList()) } } else Pair(wrapper.data, null) @@ -120,31 +122,27 @@ class MediaDetailScreenViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000) ) - private val _selectedDanBangumiSearchFlow = MutableStateFlow(null) + private val _selectedDanmakuMediaSearchFlow = MutableStateFlow(null) - private val _danBangumiDetailFlow = combine( - _danBangumiListFlow, - _selectedDanBangumiSearchFlow - ) { danBangumiListPair, selected -> - val list = danBangumiListPair.second - val anime = if (!list.isNullOrEmpty()) { - selected?.let { list.find { it.animeId == selected.animeId } } ?: list.firstOrNull() + private val _danmakuMediaDetailFlow = combine( + _danmakuMediaListFlow, + _selectedDanmakuMediaSearchFlow + ) { danmakuMediaListPair, selected -> + val list = danmakuMediaListPair.second + val danmakuMedia = if (!list.isNullOrEmpty()) { + selected?.let { + list.find { it.provider == selected.provider && it.mediaId == selected.mediaId } + } ?: list.firstOrNull() } else null - val danBangumiDetail = if (anime != null) { + val detail = danmakuMedia?.let { try { - val resp = danDanPlayApiService.getAnime(anime.animeId) - if (resp.errorCode == 0) { - resp.bangumi - } else { - Timber.d("danDanPlayApiService.getAnime(${anime.animeId}) ${resp.errorMessage}") - null - } + danmakuService.getMediaEpisodes(media = danmakuMedia) } catch (throwable: Throwable) { Timber.e(throwable) null } - } else null - Pair(danBangumiListPair.first, danBangumiDetail) + } + Pair(danmakuMediaListPair.first, detail) } fun refreshMediaDetail(navItem: NavigationItems.Detail) { @@ -186,15 +184,23 @@ class MediaDetailScreenViewModel @Inject constructor( } } - fun changeDanBangumi(selected: BangumiSearch) { + fun getDanmakuProviders() = danmakuService.getProviders() + + fun changeDanmakuProvider(selected: String) { + viewModelScope.launch { + _selectedDanmakuProviderFlow.emit(selected) + } + } + + fun changeDanmakuMedia(selected: DanmakuMedia) { viewModelScope.launch { - _selectedDanBangumiSearchFlow.emit(selected) + _selectedDanmakuMediaSearchFlow.emit(selected) } } - fun searchDanBangumi(query: String) { + fun searchDanmakuMedia(query: String) { viewModelScope.launch { - _banBangumiSearchQueryFlow.emit(query) + _danmakuMediaSearchQueryFlow.emit(query) } } @@ -240,9 +246,9 @@ class MediaDetailScreenViewModel @Inject constructor( _mediaDetailFlow, _favoriteFlow, _mediaProgressFlow, - _danBangumiListFlow, - _danBangumiDetailFlow - ) { wrapper, favoritePair, progressMapPair, danBangumiListPair, danBangumiInfoPair -> + _danmakuMediaListFlow, + _danmakuMediaDetailFlow + ) { wrapper, favoritePair, progressMapPair, danmakuMediaListPair, danmakuMediaDetailPair -> if (progressMapPair.first == null) { Timber.e(wrapper.error, wrapper.error?.message ?: "error") MediaDetailScreenUiState.Error( @@ -250,16 +256,17 @@ class MediaDetailScreenViewModel @Inject constructor( exception = wrapper.error ) } else if (wrapper.data != null && favoritePair.first == wrapper.data - && progressMapPair.first == wrapper.data && danBangumiListPair.first == wrapper.data - && danBangumiInfoPair.first == wrapper.data + && progressMapPair.first == wrapper.data && danmakuMediaListPair.first == wrapper.data + && danmakuMediaDetailPair.first == wrapper.data ) { MediaDetailScreenUiState.Ready( pluginInfo = wrapper.data.first, mediaDetail = wrapper.data.third, favorite = favoritePair.second != null, progressMap = progressMapPair.second, - danBangumiList = danBangumiListPair.second, - danBangumiInfo = danBangumiInfoPair.second + danmakuProviders = danmakuService.getProviders(), + danmakuMediaList = danmakuMediaListPair.second, + danmakuMediaInfo = danmakuMediaDetailPair.second ) } else null }.filterNotNull().collect { @@ -277,8 +284,9 @@ sealed interface MediaDetailScreenUiState { val mediaDetail: MediaDetail, val favorite: Boolean, val progressMap: Map, - val danBangumiList: List?, // 为null表示插件不支持dandanplay - val danBangumiInfo: BangumiInfo?, + val danmakuProviders: Set, + val danmakuMediaList: List?, // 为null表示插件禁用了弹幕搜索 + val danmakuMediaInfo: DanmakuMedia?, ) : MediaDetailScreenUiState } diff --git a/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailWidget.kt b/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailWidget.kt index a90535f..21c8f73 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailWidget.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/detail/MediaDetailWidget.kt @@ -55,8 +55,7 @@ import com.muedsa.compose.tv.widget.TwoSideWideButton import com.muedsa.compose.tv.widget.rememberScreenBackgroundState import com.muedsa.tvbox.api.data.MediaDetail import com.muedsa.tvbox.api.data.MediaMergingHttpSource -import com.muedsa.tvbox.model.dandanplay.BangumiInfo -import com.muedsa.tvbox.model.dandanplay.BangumiSearch +import com.muedsa.tvbox.model.DanmakuMedia import com.muedsa.tvbox.plugin.PluginInfo import com.muedsa.tvbox.room.model.EpisodeProgressModel import com.muedsa.tvbox.screens.NavigationItems @@ -65,7 +64,6 @@ import com.muedsa.tvbox.screens.nav import com.muedsa.tvbox.screens.plugin.home.MediaCardRow import com.muedsa.tvbox.theme.FavoriteIconColor import com.muedsa.tvbox.tool.LenientJson -import kotlinx.serialization.encodeToString import timber.log.Timber const val INIT_FOCUSED_ITEM_KEY_MEDIA_DETAIL = "MEDIA_DETAIL_TOP" @@ -76,9 +74,9 @@ fun MediaDetailWidget( mediaDetail: MediaDetail, favorite: Boolean, progressMap: Map, - danBangumiList: List?, - danBangumiInfo: BangumiInfo?, - mediaDetailScreenViewModel: MediaDetailScreenViewModel + danmakuMediaList: List?, + danmakuMediaInfo: DanmakuMedia?, + mediaDetailScreenViewModel: MediaDetailScreenViewModel, ) { val configuration = LocalConfiguration.current val screenHeight = configuration.screenHeightDp.dp @@ -223,27 +221,34 @@ fun MediaDetailWidget( } } - // 切换弹弹Play匹配剧集 - if (danBangumiList != null) { + // 切换弹幕匹配剧集 + if (danmakuMediaList != null) { item { + DanmakuProviderSelectorWidget( + mediaDetailScreenViewModel = mediaDetailScreenViewModel, + ) Text( - text = "弹弹Play匹配剧集: ", + text = "弹幕匹配: ", color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) Text( modifier = Modifier .widthIn(max = 256.dp) .basicMarquee(), text = if (enabledDanmakuState.value) - danBangumiInfo?.let { "${it.animeTitle}[Rating ${it.rating}]" } ?: "--" + danmakuMediaInfo?.let { + if (it.rating != null) { + "${it.mediaName}[Rating ${it.rating}]" + } else it.mediaName + } ?: "--" else "关闭", color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) - AnimeDanmakuSelectBtnWidget( + DanmakuMediaSelectorWidget( enabledDanmakuState = enabledDanmakuState, - mediaDetailScreenViewModel = mediaDetailScreenViewModel + mediaDetailScreenViewModel = mediaDetailScreenViewModel, ) } } @@ -255,7 +260,7 @@ fun MediaDetailWidget( onClick = { mediaDetailScreenViewModel.clearProgress( pluginInfo = pluginInfo, - mediaDetail = mediaDetail + mediaDetail = mediaDetail, ) } ) { @@ -283,16 +288,16 @@ fun MediaDetailWidget( val episodeList = episodePlaySource?.episodeList ?: emptyList() if (episodePlaySource != null && episodeList.isNotEmpty()) { item(contentType = "MEDIA_EPISODES") { - val episodeRelationMap = remember { mutableStateMapOf() } + val episodeRelationMap = remember { mutableStateMapOf() } var episodeClickLoading by remember { mutableStateOf(false) } EpisodeListWidget( episodeList = episodeList, - danEpisodeList = danBangumiInfo?.episodes ?: emptyList(), + danmakuEpisodeList = danmakuMediaInfo?.episodes ?: emptyList(), episodeProgressMap = progressMap, episodeRelationMap = episodeRelationMap, enabled = !episodeClickLoading, - onEpisodeClick = { episode, danEpisode -> + onEpisodeClick = { episode, danmakuEpisode -> episodeClickLoading = true Timber.d("click episode ${mediaDetail.id}-${episode.name}") @@ -309,8 +314,8 @@ fun MediaDetailWidget( pluginPackage = pluginInfo.packageName, mediaId = mediaDetail.id, episodeId = episode.id, - danEpisodeId = if (enabledDanmakuState.value && danEpisode != null) - danEpisode.episodeId else -1, + danmakuEpisodeJson = if (enabledDanmakuState.value && danmakuEpisode != null) + LenientJson.encodeToString(danmakuEpisode) else null, disableEpisodeProgression = mediaDetail.disableEpisodeProgression, enableCustomDanmakuList = mediaDetail.enableCustomDanmakuList, enableCustomDanmakuFlow = mediaDetail.enableCustomDanmakuFlow, diff --git a/app/src/main/java/com/muedsa/tvbox/screens/playback/PlaybackScreenViewModel.kt b/app/src/main/java/com/muedsa/tvbox/screens/playback/PlaybackScreenViewModel.kt index 928bbb4..0d58543 100644 --- a/app/src/main/java/com/muedsa/tvbox/screens/playback/PlaybackScreenViewModel.kt +++ b/app/src/main/java/com/muedsa/tvbox/screens/playback/PlaybackScreenViewModel.kt @@ -3,15 +3,15 @@ package com.muedsa.tvbox.screens.playback import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kuaishou.akdanmaku.data.DanmakuItemData -import com.muedsa.tvbox.BuildConfig import com.muedsa.tvbox.api.data.DanmakuDataFlow import com.muedsa.tvbox.api.data.MediaEpisode +import com.muedsa.tvbox.danmaku.DanmakuService import com.muedsa.tvbox.model.AppSettingModel +import com.muedsa.tvbox.model.DanmakuEpisode import com.muedsa.tvbox.plugin.PluginManager import com.muedsa.tvbox.room.dao.EpisodeProgressDao import com.muedsa.tvbox.room.model.EpisodeProgressModel import com.muedsa.tvbox.screens.NavigationItems -import com.muedsa.tvbox.service.DanDanPlayApiService import com.muedsa.tvbox.store.DataStoreRepo import com.muedsa.tvbox.tool.LenientJson import dagger.hilt.android.lifecycle.HiltViewModel @@ -30,7 +30,7 @@ import javax.inject.Inject @HiltViewModel class PlaybackScreenViewModel @Inject constructor( - private val danDanPlayApiService: DanDanPlayApiService, + private val danmakuService: DanmakuService, dateStoreRepo: DataStoreRepo, private val episodeProgressDao: EpisodeProgressDao ) : ViewModel() { @@ -71,38 +71,10 @@ class PlaybackScreenViewModel @Inject constructor( danmakuStyle = data.danmakuStyle ) } - } else if (!BuildConfig.DANDANPLAY_APP_ID.isEmpty() - && !BuildConfig.DANDANPLAY_APP_SECRET.isEmpty() - && it.danEpisodeId > 0 - ) { - danDanPlayApiService.getComment( - episodeId = it.danEpisodeId, - from = 0, - withRelated = true, - chConvert = 1 - ).comments.map { - val propArr = it.p.split(",") - val pos = (propArr[0].toFloat() * 1000).toLong() - val mode = if (propArr[1] == "1") - DanmakuItemData.DANMAKU_MODE_ROLLING - else if (propArr[1] == "4") - DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM - else if (propArr[1] == "5") - DanmakuItemData.DANMAKU_MODE_CENTER_TOP - else - DanmakuItemData.DANMAKU_MODE_ROLLING - val colorInt = propArr[2].toInt() - DanmakuItemData( - danmakuId = it.cid, - position = pos, - content = it.m, - mode = mode, - textSize = 25, - textColor = colorInt, - score = 9, - danmakuStyle = DanmakuItemData.DANMAKU_STYLE_NONE - ) - } + } else if (!it.danmakuEpisodeJson.isNullOrEmpty()) { + val danmakuEpisode = + LenientJson.decodeFromString(it.danmakuEpisodeJson) + danmakuService.getEpisodeDanmakuList(danmakuEpisode) } else emptyList() val danmakuDataFlow = if (it.enableCustomDanmakuFlow) { plugin.mediaDetailService.getEpisodeDanmakuDataFlow(mediaEpisode) diff --git a/app/src/main/java/com/muedsa/util/OkHttpCacheInterceptor.kt b/app/src/main/java/com/muedsa/util/OkHttpCacheInterceptor.kt new file mode 100644 index 0000000..1a2c198 --- /dev/null +++ b/app/src/main/java/com/muedsa/util/OkHttpCacheInterceptor.kt @@ -0,0 +1,28 @@ +package com.muedsa.util + +import okhttp3.Interceptor +import okhttp3.Response + +class OkHttpCacheInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var rep = chain.request() + val focusedCacheControl = rep.header(HEADER) + if (!focusedCacheControl.isNullOrEmpty()) { + rep = rep.newBuilder().removeHeader(HEADER).build() + } + var resp = chain.proceed(rep) + + if (!focusedCacheControl.isNullOrEmpty()) { + resp = resp.newBuilder() + .header("Cache-Control", focusedCacheControl) + .header("Pragma", "") + .build() + } + return resp + } + + companion object { + const val HEADER = "Focused-Cache-Control" + } +} \ No newline at end of file