diff --git a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.android.kt b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.android.kt new file mode 100644 index 00000000..b0656300 --- /dev/null +++ b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.android.kt @@ -0,0 +1,16 @@ +package dev.datlag.burningseries.ui.custom + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.multiplatform.webview.web.WebView +import com.multiplatform.webview.web.WebViewNavigator +import com.multiplatform.webview.web.WebViewState + +@Composable +actual fun PlatformWebView(state: WebViewState, navigator: WebViewNavigator, modifier: Modifier) { + WebView( + state = state, + navigator = navigator, + modifier = modifier + ) +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.kt new file mode 100644 index 00000000..d424cb3a --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.kt @@ -0,0 +1,9 @@ +package dev.datlag.burningseries.ui.custom + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.multiplatform.webview.web.WebViewNavigator +import com.multiplatform.webview.web.WebViewState + +@Composable +expect fun PlatformWebView(state: WebViewState, navigator: WebViewNavigator, modifier: Modifier = Modifier) \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/state/BrowserState.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/state/BrowserState.kt new file mode 100644 index 00000000..3f73fae0 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/state/BrowserState.kt @@ -0,0 +1,49 @@ +package dev.datlag.burningseries.ui.custom.state + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.datlag.burningseries.LocalDarkMode +import dev.datlag.burningseries.SharedRes +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun BrowserState(text: String) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val painterRes = if (LocalDarkMode.current) { + SharedRes.images.browser_dark + } else { + SharedRes.images.browser_light + } + Image( + modifier = Modifier.fillMaxWidth(0.7F), + painter = painterResource(painterRes), + contentDescription = text + ) + Text( + modifier = Modifier.fillMaxWidth(0.85F), + text = text, + fontWeight = FontWeight.SemiBold, + softWrap = true, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun BrowserState(text: StringResource) = BrowserState(text = stringResource(text)) \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt index 009fd23b..6abd623d 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt @@ -18,4 +18,9 @@ sealed class DialogConfig : Parcelable { val selected: Series.Language, val languages: List ) : DialogConfig() + + @Parcelize + data class StreamUnavailable( + val episode: Series.Episode + ) : DialogConfig() } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt index f6d6a691..81652cdf 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt @@ -13,7 +13,8 @@ import kotlinx.coroutines.flow.StateFlow interface SeriesComponent : Component { val seriesState: StateFlow - val episodeState: StateFlow + + val child: Value> val dialog: Value> val title: StateFlow @@ -22,6 +23,8 @@ interface SeriesComponent : Component { val coverHref: StateFlow val isFavorite: StateFlow + val loadingEpisodeHref: StateFlow + fun retryLoadingSeries(): Any? fun goBack() diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesConfig.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesConfig.kt new file mode 100644 index 00000000..52bfdc84 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesConfig.kt @@ -0,0 +1,14 @@ +package dev.datlag.burningseries.ui.screen.initial.series + +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize +import dev.datlag.burningseries.model.Series + +@Parcelize +sealed class SeriesConfig : Parcelable { + + @Parcelize + data class Activate( + val episode: Series.Episode + ) : SeriesConfig(), Parcelable +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt index ae312bff..d26101bf 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt @@ -71,11 +71,17 @@ import kotlin.math.abs fun SeriesScreen(component: SeriesComponent) { val href by component.commonHref.collectAsStateWithLifecycle() val dialogState by component.dialog.subscribeAsState() + val childState by component.child.subscribeAsState() SchemeTheme.setCommon(href) - when (calculateWindowSizeClass().widthSizeClass) { - WindowWidthSizeClass.Compact -> CompactScreen(component) - else -> DefaultScreen(component) + + childState.child?.also { (_, instance) -> + instance.render() + } ?: run { + when (calculateWindowSizeClass().widthSizeClass) { + WindowWidthSizeClass.Compact -> CompactScreen(component) + else -> DefaultScreen(component) + } } val scope = rememberCoroutineScope() @@ -224,7 +230,7 @@ private fun CompactScreen(component: SeriesComponent) { } } is SeriesState.Success -> { - val episodeState by component.episodeState.collectAsStateWithLifecycle() + val loadingEpisode by component.loadingEpisodeHref.collectAsStateWithLifecycle() val state = rememberLazyListState( initialFirstVisibleItemIndex = StateSaver.seriesListIndex, initialFirstVisibleItemScrollOffset = StateSaver.seriesListOffset @@ -264,7 +270,7 @@ private fun CompactScreen(component: SeriesComponent) { ) } - SeriesContent(current.series, episodeState) { + SeriesContent(current.series, loadingEpisode) { component.itemClicked(it) } } @@ -302,7 +308,7 @@ private fun DefaultScreen(component: SeriesComponent) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(2.dp) ) { - val episodeState by component.episodeState.collectAsStateWithLifecycle() + val loadingEpisode by component.loadingEpisodeHref.collectAsStateWithLifecycle() val state = rememberLazyListState( initialFirstVisibleItemIndex = StateSaver.seriesListIndex, initialFirstVisibleItemScrollOffset = StateSaver.seriesListOffset @@ -395,7 +401,7 @@ private fun DefaultScreen(component: SeriesComponent) { } } - SeriesContent(current.series, episodeState) { + SeriesContent(current.series, loadingEpisode) { component.itemClicked(it) } } @@ -412,17 +418,7 @@ private fun DefaultScreen(component: SeriesComponent) { } } -private fun LazyListScope.SeriesContent(content: Series, episodeState: EpisodeState, onEpisodeClick: (Series.Episode) -> Unit) { - val loadingEpisode = when (val current = episodeState) { - is EpisodeState.Loading -> current.episode.href - is EpisodeState.SuccessHoster -> current.episode.href - is EpisodeState.SuccessStream -> { - println(current.results) - null - } - else -> null - } - +private fun LazyListScope.SeriesContent(content: Series, loadingEpisode: String?, onEpisodeClick: (Series.Episode) -> Unit) { items(content.episodes, key = { it.href }) { episode -> EpisodeItem(episode, loadingEpisode.equals(episode.href, true)) { onEpisodeClick(episode) diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt index e9bd4799..41240535 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt @@ -2,17 +2,12 @@ package dev.datlag.burningseries.ui.screen.initial.series import androidx.compose.runtime.Composable import app.cash.sqldelight.coroutines.asFlow -import app.cash.sqldelight.coroutines.mapToOneNotNull import app.cash.sqldelight.coroutines.mapToOneOrNull import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.slot.* import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackCallback -import com.arkivanov.essenty.backhandler.BackHandler -import dev.datlag.burningseries.common.defaultScope -import dev.datlag.burningseries.common.ioDispatcher -import dev.datlag.burningseries.common.ioScope -import dev.datlag.burningseries.common.launchIO +import dev.datlag.burningseries.common.* import dev.datlag.burningseries.database.BurningSeries import dev.datlag.burningseries.model.BSUtil import dev.datlag.burningseries.model.Series @@ -22,9 +17,13 @@ import dev.datlag.burningseries.model.state.SeriesAction import dev.datlag.burningseries.model.state.SeriesState import dev.datlag.burningseries.network.state.EpisodeStateMachine import dev.datlag.burningseries.network.state.SeriesStateMachine +import dev.datlag.burningseries.ui.navigation.Component import dev.datlag.burningseries.ui.navigation.DialogComponent +import dev.datlag.burningseries.ui.screen.initial.series.activate.ActivateScreenComponent import dev.datlag.burningseries.ui.screen.initial.series.dialog.language.LanguageDialogComponent import dev.datlag.burningseries.ui.screen.initial.series.dialog.season.SeasonDialogComponent +import dev.datlag.burningseries.ui.screen.initial.series.dialog.unavailable.UnavailableDialog +import dev.datlag.burningseries.ui.screen.initial.series.dialog.unavailable.UnavailableDialogComponent import io.ktor.client.* import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock @@ -65,10 +64,29 @@ class SeriesScreenComponent( }.stateIn(ioScope(), SharingStarted.Lazily, database.burningSeriesQueries.seriesByHref(commonHref.value).executeAsOneOrNull()?.favoriteSince?.let { it > 0 } ?: false) private val episodeStateMachine by di.instance() - override val episodeState: StateFlow = episodeStateMachine.state.flowOn(ioDispatcher()).stateIn(ioScope(), SharingStarted.Lazily, EpisodeState.Waiting) + private val episodeState: StateFlow = episodeStateMachine.state.flowOn(ioDispatcher()).stateIn(ioScope(), SharingStarted.Lazily, EpisodeState.Waiting) + override val loadingEpisodeHref: StateFlow = episodeState.map { + (it as? EpisodeState.Loading)?.episode?.href ?: (it as? EpisodeState.SuccessHoster)?.episode?.href + }.stateIn(ioScope(), SharingStarted.Lazily, null) + + private val navigation = SlotNavigation() + override val child: Value> = childSlot( + source = navigation, + handleBackButton = false + ) { config, context -> + when (config) { + is SeriesConfig.Activate -> ActivateScreenComponent( + componentContext = context, + di = di, + episode = config.episode, + onGoBack = navigation::dismiss + ) + } + } private val dialogNavigation = SlotNavigation() private val _dialog = childSlot( + key = "DialogChildSlot", source = dialogNavigation ) { config, slotContext -> when (config) { @@ -92,16 +110,48 @@ class SeriesScreenComponent( loadNewLanguage(it) } ) + is DialogConfig.StreamUnavailable -> UnavailableDialogComponent( + componentContext = slotContext, + di = di, + episode = config.episode, + onDismissed = dialogNavigation::dismiss, + onActivate = { + navigation.activate(SeriesConfig.Activate(it)) + } + ) } } override val dialog: Value> = _dialog - private val backCallback = BackCallback(priority = Int.MAX_VALUE) { + private val backCallback = BackCallback { onGoBack() } init { backHandler.register(backCallback) + + ioScope().launchIO { + episodeState.collect { state -> + when (state) { + is EpisodeState.SuccessStream -> { + // ToDo("navigate to video player") + } + is EpisodeState.ErrorHoster, is EpisodeState.ErrorStream -> { + val episode = (state as? EpisodeState.ErrorHoster)?.episode + ?: (state as? EpisodeState.ErrorStream)?.episode + + if (episode != null) { + withMainContext { + showDialog( + DialogConfig.StreamUnavailable(episode) + ) + } + } + } + else -> { } + } + } + } } @Composable diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt new file mode 100644 index 00000000..52d83728 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt @@ -0,0 +1,12 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate + +import dev.datlag.burningseries.model.Series +import dev.datlag.burningseries.ui.navigation.Component + +interface ActivateComponent : Component { + + val episode: Series.Episode + val scrapingJs: String + + fun back() +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt new file mode 100644 index 00000000..8c326c4a --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt @@ -0,0 +1,64 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.multiplatform.webview.web.rememberWebViewNavigator +import com.multiplatform.webview.web.rememberWebViewState +import dev.icerock.moko.resources.compose.stringResource +import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.common.withIOContext +import dev.datlag.burningseries.model.BSUtil +import dev.datlag.burningseries.ui.custom.PlatformWebView +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ActivateScreen(component: ActivateComponent) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = stringResource(SharedRes.strings.activate_hint)) + }, + navigationIcon = { + IconButton( + onClick = { + component.back() + } + ) { + Icon( + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = stringResource(SharedRes.strings.back) + ) + } + } + ) + } + ) { + Box(modifier = Modifier.padding(it)) { + val state = rememberWebViewState(url = BSUtil.getBurningSeriesLink(component.episode.href)) + val navigator = rememberWebViewNavigator() + + PlatformWebView(state, navigator, Modifier.fillMaxSize()) + + LaunchedEffect(state) { + withIOContext { + do { + delay(3000) + navigator.evaluateJavaScript(component.scrapingJs) { result -> + println("Got: $result") + } + } while (isActive) + } + } + } + } +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt new file mode 100644 index 00000000..3fd2b5c7 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt @@ -0,0 +1,35 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.backhandler.BackCallback +import dev.datlag.burningseries.model.Series +import org.kodein.di.DI +import dev.datlag.burningseries.SharedRes + +class ActivateScreenComponent( + componentContext: ComponentContext, + override val di: DI, + override val episode: Series.Episode, + private val onGoBack: () -> Unit +) : ActivateComponent, ComponentContext by componentContext { + + override val scrapingJs: String = SharedRes.assets.scrape_hoster.readText() + + private val backCallback = BackCallback { + onGoBack() + } + + init { + backHandler.register(backCallback) + } + + @Composable + override fun render() { + ActivateScreen(this) + } + + override fun back() { + onGoBack() + } +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableComponent.kt new file mode 100644 index 00000000..4b0ebb8d --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableComponent.kt @@ -0,0 +1,11 @@ +package dev.datlag.burningseries.ui.screen.initial.series.dialog.unavailable + +import dev.datlag.burningseries.model.Series +import dev.datlag.burningseries.ui.navigation.DialogComponent + +interface UnavailableComponent : DialogComponent { + + val episode: Series.Episode + + fun activate() +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableDialog.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableDialog.kt new file mode 100644 index 00000000..0d409e3e --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableDialog.kt @@ -0,0 +1,46 @@ +package dev.datlag.burningseries.ui.screen.initial.series.dialog.unavailable + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.style.TextOverflow +import dev.datlag.burningseries.SharedRes +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun UnavailableDialog(component: UnavailableComponent) { + AlertDialog( + onDismissRequest = { + component.dismiss() + }, + title = { + Text( + text = stringResource(SharedRes.strings.stream_unavailable_title), + style = MaterialTheme.typography.headlineMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + softWrap = true + ) + }, + text = { + Text(text = stringResource(SharedRes.strings.stream_unavailable_text)) + }, + confirmButton = { + Button( + onClick = { + component.activate() + } + ) { + Text(text = stringResource(SharedRes.strings.activate)) + } + }, + dismissButton = { + Button( + onClick = { + component.dismiss() + } + ) { + Text(text = stringResource(SharedRes.strings.later)) + } + } + ) +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableDialogComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableDialogComponent.kt new file mode 100644 index 00000000..67b903d6 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/unavailable/UnavailableDialogComponent.kt @@ -0,0 +1,29 @@ +package dev.datlag.burningseries.ui.screen.initial.series.dialog.unavailable + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import dev.datlag.burningseries.model.Series +import org.kodein.di.DI + +class UnavailableDialogComponent( + componentContext: ComponentContext, + override val di: DI, + override val episode: Series.Episode, + private val onDismissed: () -> Unit, + private val onActivate: (Series.Episode) -> Unit +) : UnavailableComponent, ComponentContext by componentContext { + + @Composable + override fun render() { + UnavailableDialog(this) + } + + override fun dismiss() { + onDismissed() + } + + override fun activate() { + onActivate(episode) + dismiss() + } +} \ No newline at end of file diff --git a/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js b/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js new file mode 100644 index 00000000..d6be2b79 --- /dev/null +++ b/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js @@ -0,0 +1,31 @@ +(function() { + const hosterTabs = document.getElementsByClassName("hoster-tabs"); + let activeHoster = null; + if ((hosterTabs !== null && hosterTabs !== undefined) && hosterTabs.length > 0) { + const activeTabs = hosterTabs[0].getElementsByClassName("active"); + if ((activeTabs !== null && activeTabs !== undefined) && activeTabs.length > 0) { + const hosters = activeTabs[0].getElementsByTagName("a"); + if ((hosters !== null && hosters !== undefined) && hosters.length > 0) { + activeHoster = hosters[0].getAttribute("href"); + } + } + } + + const hosterPlayers = document.getElementsByClassName("hoster-player"); + if ((hosterPlayers !== null && hosterPlayers !== undefined) && hosterPlayers.length > 0) { + const player = hosterPlayers[0]; + const links = player.getElementsByTagName("a"); + if ((links !== null && links !== undefined) && links.length > 0) { + const link = links[0].getAttribute("href"); + return {href: activeHoster, url: link, embed: false}; + } else { + const frames = player.getElementsByTagName("iframe"); + if ((frames !== null && frames !== undefined) && frames.length > 0) { + const link = frames[0].getAttribute("src"); + return {href: activeHoster, url: link, embed: true}; + } else { + return null; + } + } + } +})(); \ No newline at end of file diff --git a/app/shared/src/commonMain/resources/MR/base/strings.xml b/app/shared/src/commonMain/resources/MR/base/strings.xml index 77684fba..ead3074a 100644 --- a/app/shared/src/commonMain/resources/MR/base/strings.xml +++ b/app/shared/src/commonMain/resources/MR/base/strings.xml @@ -28,4 +28,10 @@ Confirm Close Clear + Stream unavailable + There is no stream available, you can activate them now in this app or use using the browser extension on your PC. + Activate + Activate episodes by loading them once + Later + Browser initializing, please wait \ No newline at end of file diff --git a/app/shared/src/commonMain/resources/MR/images/browser_dark.svg b/app/shared/src/commonMain/resources/MR/images/browser_dark.svg new file mode 100644 index 00000000..ed34b141 --- /dev/null +++ b/app/shared/src/commonMain/resources/MR/images/browser_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/shared/src/commonMain/resources/MR/images/browser_light.svg b/app/shared/src/commonMain/resources/MR/images/browser_light.svg new file mode 100644 index 00000000..a36f068a --- /dev/null +++ b/app/shared/src/commonMain/resources/MR/images/browser_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.desktop.kt b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.desktop.kt new file mode 100644 index 00000000..34b8a5f1 --- /dev/null +++ b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.desktop.kt @@ -0,0 +1,27 @@ +package dev.datlag.burningseries.ui.custom + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.multiplatform.webview.web.WebView +import com.multiplatform.webview.web.WebViewNavigator +import com.multiplatform.webview.web.WebViewState +import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.other.CEFState +import dev.datlag.burningseries.other.LocalCEFInitialization +import dev.datlag.burningseries.ui.custom.state.BrowserState + +@Composable +actual fun PlatformWebView(state: WebViewState, navigator: WebViewNavigator, modifier: Modifier) { + val cefInitState by LocalCEFInitialization.current + + if (cefInitState is CEFState.INITIALIZED) { + WebView( + state = state, + navigator = navigator, + modifier = modifier + ) + } else { + BrowserState(SharedRes.strings.browser_initializing) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b033eb73..b56c0138 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ splashscreen = "1.0.1" sqldelight = "2.0.0" turbine = "1.0.0" versions = "0.49.0" -webview = "1.7.0" +webview = "1.7.2" windowsize-multiplatform = "0.3.1" window-styler = "0.3.2" diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt index 8968ac7d..736a5d45 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt @@ -77,21 +77,23 @@ data class Series( @SerialName("title") val title: String ) : Parcelable + @Parcelize @Serializable data class Episode( @SerialName("number") val number: String, @SerialName("title") val title: String, @SerialName("href") val href: String, @SerialName("hosters") val hosters: List - ) { + ) : Parcelable { val episodeNumber: String = BSUtil.episodeNumberRegex.find(title)?.groupValues?.lastOrNull() ?: number val episodeTitle: String = BSUtil.episodeNumberRegex.replaceFirst(title, String()).trim().ifBlank { title } + @Parcelize @Serializable data class Hoster( @SerialName("title") val title: String, @SerialName("href") val href: String - ) + ) : Parcelable } } \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt index 8d1866f6..b4f5f4bb 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt @@ -8,8 +8,8 @@ sealed interface EpisodeState { data class Loading(val episode: Series.Episode) : EpisodeState data class SuccessHoster(val episode: Series.Episode, val results: Collection) : EpisodeState data class SuccessStream(val results: Collection) : EpisodeState - data object ErrorHoster : EpisodeState - data object ErrorStream : EpisodeState + data class ErrorHoster(val episode: Series.Episode) : EpisodeState + data class ErrorStream(val episode: Series.Episode) : EpisodeState } sealed interface EpisodeAction { diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt index 81715c32..01ab0686 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt @@ -86,7 +86,7 @@ class EpisodeStateMachine( ) ) } } else { - state.override { EpisodeState.ErrorHoster } + state.override { EpisodeState.ErrorHoster(state.snapshot.episode) } } } } @@ -101,7 +101,7 @@ class EpisodeStateMachine( }.filterNotNull() if (streams.isEmpty()) { - state.override { EpisodeState.ErrorStream } + state.override { EpisodeState.ErrorStream(state.snapshot.episode) } } else { state.override { EpisodeState.SuccessStream(streams) } }