diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/WebPlayerActivity.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/WebPlayerActivity.kt index cee362a73..b90f6667a 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/WebPlayerActivity.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/WebPlayerActivity.kt @@ -6,7 +6,10 @@ import android.content.Intent import android.net.http.SslError import android.os.Bundle import android.view.WindowManager -import android.webkit.* +import android.webkit.SslErrorHandler +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView import by.kirich1409.viewbindingdelegate.viewBinding import ru.radiationx.anilibria.R import ru.radiationx.anilibria.apptheme.AppTheme @@ -81,7 +84,6 @@ class WebPlayerActivity : BaseActivity(R.layout.activity_moon) { binding.webView.settings.apply { cacheMode = WebSettings.LOAD_NO_CACHE - javaScriptEnabled = true } val webViewClient = object : WebViewClientCompat() { diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/PlayerViewModel.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/PlayerViewModel.kt index 2a48e7e50..0572a1f71 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/PlayerViewModel.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/PlayerViewModel.kt @@ -26,14 +26,10 @@ import ru.radiationx.anilibria.ui.activities.player.models.LoadingState import ru.radiationx.anilibria.ui.activities.player.models.PlayerAction import ru.radiationx.anilibria.ui.activities.player.models.PlayerData import ru.radiationx.anilibria.ui.activities.player.models.PlayerDataState -import ru.radiationx.anilibria.ui.activities.player.models.PlayerRelease import ru.radiationx.data.datasource.holders.EpisodesCheckerHolder import ru.radiationx.data.datasource.holders.PreferencesHolder import ru.radiationx.data.entity.common.PlayerQuality -import ru.radiationx.data.entity.domain.release.EpisodeAccess -import ru.radiationx.data.entity.domain.release.Release import ru.radiationx.data.entity.domain.types.EpisodeId -import ru.radiationx.data.entity.domain.types.ReleaseId import ru.radiationx.data.interactors.ReleaseInteractor import ru.radiationx.data.repository.ReleaseRepository import ru.radiationx.shared.ktx.coRunCatching @@ -114,7 +110,7 @@ class PlayerViewModel( fun onSettingsClick() { launchWithData { data -> - val episode = data.episodes.find { it.id == _episodeId.value } ?: return@launchWithData + val episode = data.getEpisode(_episodeId.value) ?: return@launchWithData val quality = preferencesHolder.playerQuality.value val settingsState = PlayerSettingsState( currentSpeed = preferencesHolder.playSpeed.value, @@ -162,13 +158,7 @@ class PlayerViewModel( @OptIn(DelicateCoroutinesApi::class) fun saveEpisodeSeek(episodeId: EpisodeId, seek: Long) { GlobalScope.launch { - val access = EpisodeAccess( - id = episodeId, - seek = seek, - isViewed = true, - lastAccess = System.currentTimeMillis() - ) - episodesCheckerHolder.putEpisode(access) + releaseInteractor.setAccessSeek(episodeId, seek) } } @@ -203,7 +193,9 @@ class PlayerViewModel( _dataJob = viewModelScope.launch { _dataState.update { LoadingState(loading = true) } coRunCatching { - loadAllData(episodeId) + releaseInteractor + .loadWithFranchises(episodeId.releaseId) + .map { it.toPlayerRelease() } }.onSuccess { releases -> _dataState.update { it.copy(data = PlayerData(releases)) } playEpisode(episodeId) @@ -226,31 +218,5 @@ class PlayerViewModel( } } - private suspend fun loadAllData(episodeId: EpisodeId): List { - val rootRelease = requireNotNull(releaseInteractor.getFull(episodeId.releaseId)) { - "Loaded release is null for $episodeId" - } - val rootReleaseIds = mutableListOf() - rootRelease.franchises.forEach { franchise -> - franchise.releases.forEach { - rootReleaseIds.add(it.id) - } - } - if (rootReleaseIds.isEmpty()) { - return listOf(rootRelease.toPlayerRelease()) - } - val idsToLoad = rootReleaseIds.filter { it != rootRelease.id } - val franchiseReleases = releaseRepository.getFullReleasesById(idsToLoad) - - val allReleasesMap = mutableMapOf() - allReleasesMap[rootRelease.id] = rootRelease - franchiseReleases.forEach { - allReleasesMap[it.id] = it - } - return rootReleaseIds.mapNotNull { - allReleasesMap[it]?.toPlayerRelease() - } - } - } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/VideoPlayerActivity.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/VideoPlayerActivity.kt index 0a2070e08..1c86cc9b9 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/VideoPlayerActivity.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/VideoPlayerActivity.kt @@ -293,12 +293,16 @@ class VideoPlayerActivity : BaseActivity(R.layout.activity_videoplayer) { binding.playerView.onInteraction() } - private fun handleEpisode(intent: Intent, bundle: Bundle?, isNew: Boolean) { + private fun handleEpisode(intent: Intent, bundle: Bundle?, isNewIntent: Boolean) { val intentEpisodeId = intent.getExtraNotNull(ARG_EPISODE_ID) val savedEpisodeId = bundle?.getExtra(KEY_EPISODE_ID) - val episodeId = savedEpisodeId ?: intentEpisodeId + val episodeId = if (isNewIntent) { + intentEpisodeId + } else { + savedEpisodeId ?: intentEpisodeId + } analytics.handleEpisode( - isNewIntent = isNew, + isNewIntent = isNewIntent, hasBundle = bundle != null, episodeId = episodeId ) diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/mappers/Mapper.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/mappers/Mapper.kt index 2f4a1852a..4549fa4fc 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/mappers/Mapper.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/mappers/Mapper.kt @@ -20,8 +20,8 @@ fun Release.toPlayerRelease() = PlayerRelease( episodes = episodes.asReversed() ) -fun PlayerData.toDataState(episodeId: EpisodeId): PlayerDataState { - return getRelease(episodeId.releaseId).toDataState(episodeId) +fun PlayerData.toDataState(episodeId: EpisodeId): PlayerDataState? { + return getRelease(episodeId.releaseId)?.toDataState(episodeId) } fun PlayerRelease.toDataState(episodeId: EpisodeId) = PlayerDataState( diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/models/PlayerData.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/models/PlayerData.kt index b93214b9f..508357b85 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/models/PlayerData.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/player/models/PlayerData.kt @@ -9,16 +9,12 @@ data class PlayerData( ) { val episodes: List = releases.flatMap { it.episodes } - fun getRelease(releaseId: ReleaseId): PlayerRelease { - return requireNotNull(releases.find { it.id == releaseId }) { - "No loaded release for id ${releaseId.id} in ${releases.map { it.id.id }}" - } + fun getRelease(releaseId: ReleaseId): PlayerRelease? { + return releases.find { it.id == releaseId } } - fun getEpisode(episodeId: EpisodeId): Episode { - return requireNotNull(episodes.find { it.id == episodeId }) { - "No loaded episode for id $episodeId" - } + fun getEpisode(episodeId: EpisodeId): Episode? { + return episodes.find { it.id == episodeId } } } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/widgets/ExtendedWebView.java b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/widgets/ExtendedWebView.java index eb541e668..9a1e73a90 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/widgets/ExtendedWebView.java +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/widgets/ExtendedWebView.java @@ -1,7 +1,6 @@ package ru.radiationx.anilibria.ui.widgets; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Handler; @@ -92,6 +91,8 @@ public void init() { settings.setDefaultFontSize(16); settings.setTextZoom(100); settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); settings.setAllowFileAccess(true); settings.setAllowContentAccess(true); settings.setAllowFileAccessFromFileURLs(true); diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailRelatedViewModel.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailRelatedViewModel.kt index f4fee4b15..c81e48286 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailRelatedViewModel.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailRelatedViewModel.kt @@ -2,25 +2,20 @@ package ru.radiationx.anilibria.screen.details import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.toList import ru.radiationx.anilibria.common.BaseCardsViewModel import ru.radiationx.anilibria.common.CardsDataConverter import ru.radiationx.anilibria.common.LibriaCard import ru.radiationx.anilibria.common.LibriaCardRouter import ru.radiationx.data.interactors.ReleaseInteractor -import ru.radiationx.data.repository.ReleaseRepository import toothpick.InjectConstructor @InjectConstructor class DetailRelatedViewModel( argExtra: DetailExtra, private val releaseInteractor: ReleaseInteractor, - private val releaseRepository: ReleaseRepository, private val converter: CardsDataConverter, private val cardRouter: LibriaCardRouter, ) : BaseCardsViewModel() { @@ -44,20 +39,9 @@ class DetailRelatedViewModel( } override suspend fun getLoader(requestPage: Int): List { - val release = releaseInteractor.getFull(releaseId) ?: releaseInteractor.getItem(releaseId) - val releaseCodes = DetailsViewModel.getReleasesFromDesc(release?.description.orEmpty()) - if (releaseCodes.isEmpty()) { - return emptyList() - } - - val singles = releaseCodes.map { - flow { emit(releaseRepository.getRelease(it)) } - } - return merge(*singles.toTypedArray()).toList() - .let { releases -> - releaseInteractor.updateItemsCache(releases) - releases.map { converter.toCard(it) } - } + val releases = releaseInteractor.loadWithFranchises(releaseId).filter { it.id != releaseId } + releaseInteractor.updateItemsCache(releases) + return releases.map { converter.toCard(it) } } override fun hasMoreCards(newCards: List, allCards: List): Boolean = diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailsViewModel.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailsViewModel.kt index 7f386937b..ed7be2b5e 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailsViewModel.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/details/DetailsViewModel.kt @@ -26,19 +26,9 @@ class DetailsViewModel( ) : BaseRowsViewModel() { companion object { - private val linkPattern = Regex("\\/release\\/([^.]+?)\\.html") - const val RELEASE_ROW_ID = 1L const val RELATED_ROW_ID = 2L const val RECOMMENDS_ROW_ID = 3L - - fun getReleasesFromDesc(description: String): List { - return linkPattern - .findAll(description) - .map { it.groupValues[1] } - .map { ReleaseCode(it) } - .toList() - } } private val releaseId = argExtra.id @@ -65,9 +55,9 @@ class DetailsViewModel( emit(it) } } - .map { it.description.orEmpty() } - .distinctUntilChanged() - .map { getReleasesFromDesc(it) } + .map { release -> + release.getFranchisesIds().filter { it != release.id } + } .distinctUntilChanged() .onEach { updateAvailableRow(RELATED_ROW_ID, it.isNotEmpty()) diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerController.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerController.kt index 431ffc7db..264a7bf12 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerController.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerController.kt @@ -1,5 +1,7 @@ package ru.radiationx.anilibria.screen.player +import kotlinx.coroutines.flow.MutableStateFlow +import ru.radiationx.data.entity.domain.release.Release import ru.radiationx.data.entity.domain.types.EpisodeId import ru.radiationx.shared.ktx.EventFlow import toothpick.InjectConstructor @@ -7,5 +9,11 @@ import toothpick.InjectConstructor @InjectConstructor class PlayerController { + val data = MutableStateFlow?>(null) + val selectEpisodeRelay = EventFlow() + + fun reset() { + data.value = null + } } \ No newline at end of file diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerFragment.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerFragment.kt index 3df46620f..42704ca15 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerFragment.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerFragment.kt @@ -52,8 +52,11 @@ class PlayerFragment : BasePlayerFragment() { override fun onSpeedClick() = viewModel.onSpeedClick() override fun onEpisodesClick() = viewModel.onEpisodesClick(getPosition()) } + progressBarManager.initialDelay = 0 + progressBarManager.show() subscribeTo(viewModel.videoData.filterNotNull()) { + progressBarManager.hide() playerGlue?.apply { title = it.title subtitle = it.subtitle diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerViewModel.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerViewModel.kt index 351ce592f..fd9242b1c 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerViewModel.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/PlayerViewModel.kt @@ -18,6 +18,7 @@ import ru.radiationx.data.entity.domain.release.Episode import ru.radiationx.data.entity.domain.release.Release import ru.radiationx.data.interactors.ReleaseInteractor import ru.radiationx.shared.ktx.EventFlow +import ru.radiationx.shared.ktx.coRunCatching import toothpick.InjectConstructor @InjectConstructor @@ -26,7 +27,7 @@ class PlayerViewModel( private val releaseInteractor: ReleaseInteractor, private val preferencesHolder: PreferencesHolder, private val guidedRouter: GuidedRouter, - playerController: PlayerController, + private val playerController: PlayerController, ) : LifecycleViewModel() { val videoData = MutableStateFlow(null) @@ -35,12 +36,13 @@ class PlayerViewModel( val playAction = EventFlow() private var currentEpisodes = mutableListOf() - private var currentRelease: Release? = null + private var currentReleases: List? = null private var currentEpisode: Episode? = null private var currentQuality: PlayerQuality? = null private var currentComplete: Boolean? = null init { + playerController.reset() qualityState.value = preferencesHolder.playerQuality.value speedState.value = preferencesHolder.playSpeed.value @@ -69,18 +71,33 @@ class PlayerViewModel( } .launchIn(viewModelScope) - releaseInteractor - .observeFull(argExtra.releaseId) - .onEach { release -> - currentRelease = release + viewModelScope.launch { + coRunCatching { + releaseInteractor.loadWithFranchises(argExtra.releaseId) + }.onSuccess { releases -> + playerController.data.value = releases + currentReleases = releases currentEpisodes.clear() - currentEpisodes.addAll(release.episodes.reversed()) + currentEpisodes.addAll(releases.flatMap { it.episodes.reversed() }) val episodeId = currentEpisode?.id ?: argExtra.episodeId - val episode = currentEpisodes.firstOrNull { it.id == episodeId } + val episode = currentEpisodes + .firstOrNull { it.id == episodeId } ?: currentEpisodes.firstOrNull() episode?.also { playEpisode(it) } + }.onFailure { + } - .launchIn(viewModelScope) + } + } + + override fun onCleared() { + super.onCleared() + playerController.reset() + } + + private fun getCurrentRelease(): Release? { + val episodeId = currentEpisode?.id ?: return null + return currentReleases?.find { it.id == episodeId.releaseId } } fun onPauseClick(position: Long) { @@ -102,7 +119,7 @@ class PlayerViewModel( } fun onEpisodesClick(position: Long) { - val release = currentRelease ?: return + val release = getCurrentRelease() ?: return val episode = currentEpisode ?: return saveEpisode(position) guidedRouter.open(PlayerEpisodesGuidedScreen(release.id, episode.id)) @@ -110,20 +127,20 @@ class PlayerViewModel( fun onQualityClick(position: Long) { - val release = currentRelease ?: return + val release = getCurrentRelease() ?: return val episode = currentEpisode ?: return saveEpisode(position) guidedRouter.open(PlayerQualityGuidedScreen(release.id, episode.id)) } fun onSpeedClick() { - val release = currentRelease ?: return + val release = getCurrentRelease() ?: return val episode = currentEpisode ?: return guidedRouter.open(PlayerSpeedGuidedScreen(release.id, episode.id)) } fun onComplete(position: Long) { - val release = currentRelease ?: return + val release = getCurrentRelease() ?: return val episode = currentEpisode ?: return if (currentComplete == true) return currentComplete = true @@ -138,7 +155,7 @@ class PlayerViewModel( } fun onPrepare(duration: Long) { - val release = currentRelease ?: return + val release = getCurrentRelease() ?: return val episode = currentEpisode ?: return viewModelScope.launch { val access = releaseInteractor.getAccess(episode.id) @@ -191,7 +208,7 @@ class PlayerViewModel( } private fun updateEpisode(force: Boolean = false) { - val release = currentRelease ?: return + val release = getCurrentRelease() ?: return val episode = currentEpisode ?: return val quality = currentQuality ?: return viewModelScope.launch { diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesGuidedFragment.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesGuidedFragment.kt index 9fe826b9f..142e53b65 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesGuidedFragment.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesGuidedFragment.kt @@ -12,10 +12,6 @@ import ru.radiationx.shared.ktx.android.subscribeTo class PlayerEpisodesGuidedFragment : BasePlayerGuidedFragment() { companion object { - private const val CHUNK_SIZE = 20 - private const val CHUNK_THRESHOLD = 32 - private const val CHUNK_ID_OFFSET = 100000 - private const val CHUNK_ENABLED = false } private val viewModel by viewModel { argExtra } @@ -28,59 +24,54 @@ class PlayerEpisodesGuidedFragment : BasePlayerGuidedFragment() { viewLifecycleOwner.lifecycle.addObserver(viewModel) subscribeTo(viewModel.episodesData) { - actions = if (CHUNK_ENABLED && it.size > CHUNK_THRESHOLD) { - createChunkedActions(it) - } else { - createEpisodesActions(0, it) - } + actions = createGroupedActions(it) } - subscribeTo(viewModel.selectedIndex.filterNotNull()) { selectedIndex -> - selectedActionPosition = if (actions.any { it.hasSubActions() }) { - val chunkActionId = ((selectedIndex / CHUNK_SIZE) + CHUNK_ID_OFFSET).toLong() - val chunkPosition = findActionPositionById(chunkActionId) - findActionById(chunkActionId)?.also { - expandAction(it, false) - } - chunkPosition - } else { - selectedIndex - } + subscribeTo(viewModel.selectedAction.filterNotNull()) { action -> + selectedActionPosition = findActionPositionById(action.id) } } - private fun createChunkedActions(episodes: List>): List = - episodes.chunked(CHUNK_SIZE).mapIndexed { index: Int, chunk: List> -> - val first = chunk.first().first - val last = chunk.last().first - val offset = index * CHUNK_SIZE - GuidedAction.Builder(requireContext()) - .id((CHUNK_ID_OFFSET + index).toLong()) - .title("$first – $last") - .subActions(createEpisodesActions(offset, chunk)) - .build() + private fun createGroupedActions(groups: List): List { + if (groups.size <= 1) { + return groups.getOrNull(0)?.let { createEpisodesActions(it.actions) }.orEmpty() } + return buildList { + groups.forEach { group -> + val groupAction = GuidedAction.Builder(requireContext()) + .id(group.id) + .title(group.title) + .multilineDescription(true) + .infoOnly(true) + .enabled(false) + .focusable(false) + .build() + add(groupAction) + addAll(createEpisodesActions(group.actions)) + } + } + } private fun createEpisodesActions( - offset: Int, - episodes: List>, - ): List = - episodes.mapIndexed { index: Int, data: Pair -> + episodes: List, + ): List { + return episodes.map { action -> GuidedAction.Builder(requireContext()) - .id((offset + index).toLong()) - .title(data.first) - .description(data.second) + .id(action.id) + .title(action.title) + .description(action.description) .build() } + } override fun onGuidedActionClicked(action: GuidedAction) { if (!action.hasSubActions()) { - viewModel.applyEpisode(action.id.toInt()) + viewModel.applyEpisode(action.id) } } override fun onSubGuidedActionClicked(action: GuidedAction): Boolean { - viewModel.applyEpisode(action.id.toInt()) + viewModel.applyEpisode(action.id) return super.onSubGuidedActionClicked(action) } } \ No newline at end of file diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesViewModel.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesViewModel.kt index 579495266..8a57ca0c8 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesViewModel.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/player/episodes/PlayerEpisodesViewModel.kt @@ -1,5 +1,6 @@ package ru.radiationx.anilibria.screen.player.episodes +import android.util.Log import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn @@ -9,8 +10,9 @@ import ru.radiationx.anilibria.common.fragment.GuidedRouter import ru.radiationx.anilibria.screen.LifecycleViewModel import ru.radiationx.anilibria.screen.player.PlayerController import ru.radiationx.anilibria.screen.player.PlayerExtra -import ru.radiationx.data.entity.domain.release.Episode +import ru.radiationx.data.entity.domain.release.EpisodeAccess import ru.radiationx.data.entity.domain.release.Release +import ru.radiationx.data.entity.domain.types.EpisodeId import ru.radiationx.data.interactors.ReleaseInteractor import ru.radiationx.shared.ktx.asTimeSecString import toothpick.InjectConstructor @@ -24,41 +26,88 @@ class PlayerEpisodesViewModel( private val playerController: PlayerController, ) : LifecycleViewModel() { - val episodesData = MutableStateFlow>>(emptyList()) - val selectedIndex = MutableStateFlow(null) - - private val currentEpisodes = mutableListOf() + val episodesData = MutableStateFlow>(emptyList()) + val selectedAction = MutableStateFlow(null) init { - releaseInteractor - .observeFull(argExtra.releaseId) - .onEach { - updateEpisodes(it) - } - .launchIn(viewModelScope) + val playerData = playerController.data.value + if (playerData != null) { + updateEpisodes(playerData) + } else { + releaseInteractor + .observeFull(argExtra.releaseId) + .onEach { + updateEpisodes(listOf(it)) + } + .launchIn(viewModelScope) + } } - fun applyEpisode(index: Int) { + fun applyEpisode(actionId: Long) { guidedRouter.close() - playerController.selectEpisodeRelay.emit(currentEpisodes[index].id) + val action = episodesData.value.findAction { it.id == actionId } + if (action != null) { + playerController.selectEpisodeRelay.emit(action.episodeId) + } } - private fun updateEpisodes(release: Release) { + private fun updateEpisodes(releases: List) { viewModelScope.launch { - currentEpisodes.clear() - currentEpisodes.addAll(release.episodes.reversed()) - val accesses = releaseInteractor.getAccesses(release.id).associateBy { it.id } - episodesData.value = currentEpisodes.map { - val access = accesses[it.id] + val accesses = releases + .flatMap { releaseInteractor.getAccesses(it.id) } + .associateBy { it.id } + val groups = releases.toGroups(accesses) + episodesData.value = groups + selectedAction.value = groups.findAction { it.episodeId == argExtra.episodeId } + } + } + + private fun List.findAction(block: (Action) -> Boolean): Action? { + forEach { + val action = it.actions.find(block) + if (action != null) { + return action + } + } + return null + } + + private fun List.toGroups(accesses: Map): List { + var id = 0L + return map { release -> + val groupId = id++ + val actions = release.episodes.asReversed().map { episode -> + val access = accesses[episode.id] val description = if (access != null && access.isViewed && access.seek > 0) { "Остановлена на ${Date(access.seek).asTimeSecString()}" } else { null } - Pair(it.title.orEmpty(), description) + Action( + id = id++, + episodeId = episode.id, + title = episode.title.orEmpty(), + description = description + ) } - selectedIndex.value = currentEpisodes.indexOfLast { it.id == argExtra.episodeId } + Group( + id = groupId, + title = release.title.orEmpty(), + actions = actions + ) } - } + + data class Group( + val id: Long, + val title: String, + val actions: List, + ) + + data class Action( + val id: Long, + val episodeId: EpisodeId, + val title: String, + val description: String?, + ) } \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/entity/domain/release/EpisodeAccess.kt b/data/src/main/java/ru/radiationx/data/entity/domain/release/EpisodeAccess.kt index c871e9f9e..3018bb70e 100644 --- a/data/src/main/java/ru/radiationx/data/entity/domain/release/EpisodeAccess.kt +++ b/data/src/main/java/ru/radiationx/data/entity/domain/release/EpisodeAccess.kt @@ -10,4 +10,10 @@ data class EpisodeAccess( val seek: Long, val isViewed: Boolean, val lastAccess: Long, -) : Parcelable \ No newline at end of file +) : Parcelable { + companion object { + fun createDefault(id: EpisodeId): EpisodeAccess { + return EpisodeAccess(id, 0L, false, 0L) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/entity/domain/release/Release.kt b/data/src/main/java/ru/radiationx/data/entity/domain/release/Release.kt index 748a0b004..501068302 100644 --- a/data/src/main/java/ru/radiationx/data/entity/domain/release/Release.kt +++ b/data/src/main/java/ru/radiationx/data/entity/domain/release/Release.kt @@ -39,7 +39,7 @@ data class Release( val sourceEpisodes: List, val externalPlaylists: List, val rutubePlaylist: List, - val torrents: List + val torrents: List, ) : Parcelable { @@ -56,4 +56,14 @@ data class Release( val titleEng: String? get() = names.lastOrNull() + + fun getFranchisesIds(): List { + val ids = mutableListOf() + franchises.forEach { franchise -> + franchise.releases.forEach { + ids.add(it.id) + } + } + return ids + } } diff --git a/data/src/main/java/ru/radiationx/data/entity/mapper/TeamsMapper.kt b/data/src/main/java/ru/radiationx/data/entity/mapper/TeamsMapper.kt index 43567c6a4..67fcdbe50 100644 --- a/data/src/main/java/ru/radiationx/data/entity/mapper/TeamsMapper.kt +++ b/data/src/main/java/ru/radiationx/data/entity/mapper/TeamsMapper.kt @@ -9,6 +9,7 @@ import ru.radiationx.data.entity.domain.team.Team import ru.radiationx.data.entity.domain.team.TeamRole import ru.radiationx.data.entity.domain.team.TeamUser import ru.radiationx.data.entity.domain.team.Teams +import ru.radiationx.shared.ktx.android.parseColorOrNull fun TeamsResponse.toDomain() = Teams( headerRoles = headerRoles.map { it.toDomain() }, @@ -30,7 +31,7 @@ fun TeamUserResponse.toDomain() = TeamUser( fun TeamRoleResponse.toDomain() = TeamRole( title = title, - color = color?.mapColorToMd()?.let { Color.parseColor(it) } + color = color?.mapColorToMd()?.parseColorOrNull() ) private fun String.mapColorToMd(): String = when (this) { diff --git a/data/src/main/java/ru/radiationx/data/interactors/ReleaseInteractor.kt b/data/src/main/java/ru/radiationx/data/interactors/ReleaseInteractor.kt index 7f2a6d630..0cb080ed8 100644 --- a/data/src/main/java/ru/radiationx/data/interactors/ReleaseInteractor.kt +++ b/data/src/main/java/ru/radiationx/data/interactors/ReleaseInteractor.kt @@ -97,6 +97,25 @@ class ReleaseInteractor @Inject constructor( } } + suspend fun loadWithFranchises(releaseId: ReleaseId): List { + val rootRelease = requireNotNull(getFull(releaseId)) { + "Loaded release is null for $releaseId" + } + val rootReleaseIds = rootRelease.getFranchisesIds() + if (rootReleaseIds.isEmpty()) { + return listOf(rootRelease) + } + val idsToLoad = rootReleaseIds.filter { it != rootRelease.id } + val franchiseReleases = releaseRepository.getFullReleasesById(idsToLoad) + + val allReleasesMap = mutableMapOf() + allReleasesMap[rootRelease.id] = rootRelease + franchiseReleases.forEach { + allReleasesMap[it.id] = it + } + return rootReleaseIds.mapNotNull { allReleasesMap[it] } + } + /* Common */ fun observeAccesses(releaseId: ReleaseId): Flow> { return episodesCheckerStorage.observeEpisodes().map { accesses -> @@ -124,10 +143,7 @@ class ReleaseInteractor @Inject constructor( suspend fun markUnviewed(id: EpisodeId) { updateEpisode(id) { - it.copy( - isViewed = false, - lastAccess = 0 - ) + EpisodeAccess.createDefault(id) } } @@ -142,7 +158,7 @@ class ReleaseInteractor @Inject constructor( } private suspend fun updateEpisode(id: EpisodeId, block: (EpisodeAccess) -> EpisodeAccess) { - val access = episodesCheckerStorage.getEpisode(id) ?: return + val access = episodesCheckerStorage.getEpisode(id) ?: EpisodeAccess.createDefault(id) val newAccess = block.invoke(access) episodesCheckerStorage.putEpisode(newAccess) } diff --git a/data/src/main/java/ru/radiationx/data/system/TLSSocketFactory.java b/data/src/main/java/ru/radiationx/data/system/TLSSocketFactory.java deleted file mode 100644 index 02aaf3c10..000000000 --- a/data/src/main/java/ru/radiationx/data/system/TLSSocketFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -package ru.radiationx.data.system; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; - -public class TLSSocketFactory extends SSLSocketFactory { - private final SSLSocketFactory delegate; - - public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, null, null); - delegate = context.getSocketFactory(); - } - - @Override - public String[] getDefaultCipherSuites() { - return delegate.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return delegate.getSupportedCipherSuites(); - } - - @Override - public Socket createSocket() throws IOException { - return enableTLSOnSocket(delegate.createSocket()); - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { - return enableTLSOnSocket(delegate.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket(String host, int port) throws IOException { - return enableTLSOnSocket(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { - return enableTLSOnSocket(delegate.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return enableTLSOnSocket(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort)); - } - - private Socket enableTLSOnSocket(Socket socket) { - if ((socket instanceof SSLSocket)) { - ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.2"}); - } - return socket; - } -} diff --git a/data/src/main/java/ru/radiationx/data/system/WrongHostException.kt b/data/src/main/java/ru/radiationx/data/system/WrongHostException.kt deleted file mode 100644 index bbbc32f0b..000000000 --- a/data/src/main/java/ru/radiationx/data/system/WrongHostException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ru.radiationx.data.system - -class WrongHostException(host: String) : Exception("Неверный IP адрес сервера: '$host'") \ No newline at end of file diff --git a/shared-android-ktx/src/main/java/ru/radiationx/shared/ktx/android/String.kt b/shared-android-ktx/src/main/java/ru/radiationx/shared/ktx/android/String.kt index 5268efe65..c5d81a8bd 100644 --- a/shared-android-ktx/src/main/java/ru/radiationx/shared/ktx/android/String.kt +++ b/shared-android-ktx/src/main/java/ru/radiationx/shared/ktx/android/String.kt @@ -1,5 +1,6 @@ package ru.radiationx.shared.ktx.android +import android.graphics.Color import android.util.Base64 import java.nio.charset.StandardCharsets @@ -7,4 +8,12 @@ fun String?.toBase64(): String? { return this?.let { Base64.encodeToString(it.toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) } +} + +fun String.parseColorOrNull(): Int? { + return try { + Color.parseColor(this) + } catch (ignore: Exception) { + null + } } \ No newline at end of file