mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into fea/live-tv
This commit is contained in:
commit
7f95dc44ac
6 changed files with 225 additions and 67 deletions
|
|
@ -228,6 +228,8 @@ dependencies {
|
|||
implementation(libs.timber)
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
implementation(libs.multiplatform.markdown.renderer)
|
||||
implementation(libs.multiplatform.markdown.renderer.m3)
|
||||
implementation(libs.programguide)
|
||||
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
|
|
|
|||
|
|
@ -43,8 +43,47 @@ fun SeasonCard(
|
|||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
showImageOverlay: Boolean = false,
|
||||
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f),
|
||||
) = SeasonCard(
|
||||
title = item?.title,
|
||||
subtitle = item?.subtitle,
|
||||
name = item?.name,
|
||||
imageUrl = item?.imageUrl,
|
||||
isFavorite = item?.data?.userData?.isFavorite ?: false,
|
||||
isPlayed = item?.data?.userData?.played ?: false,
|
||||
unplayedItemCount = item?.data?.userData?.unplayedItemCount ?: 0,
|
||||
playedPercentage = item?.data?.userData?.playedPercentage ?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
imageHeight = imageHeight,
|
||||
imageWidth = imageWidth,
|
||||
interactionSource = interactionSource,
|
||||
showImageOverlay = showImageOverlay,
|
||||
aspectRatio = aspectRatio,
|
||||
)
|
||||
|
||||
/**
|
||||
* A Card for a TV Show Season, but can generically show most items
|
||||
*/
|
||||
@Composable
|
||||
fun SeasonCard(
|
||||
title: String?,
|
||||
subtitle: String?,
|
||||
name: String?,
|
||||
imageUrl: String?,
|
||||
isFavorite: Boolean,
|
||||
isPlayed: Boolean,
|
||||
unplayedItemCount: Int,
|
||||
playedPercentage: Double,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
imageHeight: Dp = Dp.Unspecified,
|
||||
imageWidth: Dp = Dp.Unspecified,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
showImageOverlay: Boolean = false,
|
||||
aspectRatio: Float = (2f / 3f),
|
||||
) {
|
||||
val dto = item?.data
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
||||
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
|
||||
|
|
@ -89,13 +128,13 @@ fun SeasonCard(
|
|||
.fillMaxSize(),
|
||||
) {
|
||||
ItemCardImage(
|
||||
imageUrl = item?.imageUrl,
|
||||
name = item?.name,
|
||||
imageUrl = imageUrl,
|
||||
name = name,
|
||||
showOverlay = showImageOverlay,
|
||||
favorite = dto?.userData?.isFavorite ?: false,
|
||||
watched = dto?.userData?.played ?: false,
|
||||
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
|
||||
watchedPercent = dto?.userData?.playedPercentage,
|
||||
favorite = isFavorite,
|
||||
watched = isPlayed,
|
||||
unwatchedCount = unplayedItemCount,
|
||||
watchedPercent = playedPercentage,
|
||||
useFallbackText = false,
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
@ -111,7 +150,7 @@ fun SeasonCard(
|
|||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = item?.title ?: "",
|
||||
text = title ?: "",
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
|
|
@ -121,7 +160,7 @@ fun SeasonCard(
|
|||
.enableMarquee(focusedAfterDelay),
|
||||
)
|
||||
Text(
|
||||
text = item?.subtitle ?: "",
|
||||
text = subtitle ?: "",
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.github.damontecres.wholphin.ui.data
|
||||
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
|
||||
sealed interface Trailer {
|
||||
val name: String
|
||||
}
|
||||
|
||||
data class LocalTrailer(
|
||||
val baseItem: BaseItem,
|
||||
) : Trailer {
|
||||
override val name: String
|
||||
get() = baseItem.name ?: ""
|
||||
}
|
||||
|
||||
data class RemoteTrailer(
|
||||
override val name: String,
|
||||
val url: String,
|
||||
) : Trailer
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.movie
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -9,6 +10,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -25,16 +29,20 @@ import androidx.compose.ui.draw.alpha
|
|||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
|
|
@ -47,7 +55,6 @@ import com.github.damontecres.wholphin.data.model.chooseSource
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
|
|
@ -59,6 +66,9 @@ import com.github.damontecres.wholphin.ui.components.chooseStream
|
|||
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.data.LocalTrailer
|
||||
import com.github.damontecres.wholphin.ui.data.RemoteTrailer
|
||||
import com.github.damontecres.wholphin.ui.data.Trailer
|
||||
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
|
|
@ -96,7 +106,7 @@ class MovieViewModel
|
|||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||
val trailers = MutableLiveData<List<BaseItem>>(listOf())
|
||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||
|
||||
override fun init(
|
||||
itemId: UUID,
|
||||
|
|
@ -111,15 +121,31 @@ class MovieViewModel
|
|||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = result
|
||||
}
|
||||
val remoteTrailers =
|
||||
item.data.remoteTrailers
|
||||
?.mapNotNull { t ->
|
||||
t.url?.let { url ->
|
||||
val name =
|
||||
t.name
|
||||
// TODO would be nice to clean up the trailer name
|
||||
// ?.replace(item.name ?: "", "")
|
||||
// ?.removePrefix(" - ")
|
||||
?: "Trailer"
|
||||
RemoteTrailer(name, url)
|
||||
}
|
||||
}.orEmpty()
|
||||
.sortedBy { it.name }
|
||||
val localTrailerCount = item.data.localTrailerCount ?: 0
|
||||
if (localTrailerCount > 0) {
|
||||
val trailers =
|
||||
val localTrailers =
|
||||
if (localTrailerCount > 0) {
|
||||
api.userLibraryApi.getLocalTrailers(itemId).content.map {
|
||||
BaseItem.from(it, api)
|
||||
LocalTrailer(BaseItem.from(it, api))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@MovieViewModel.trailers.value = trailers
|
||||
} else {
|
||||
listOf()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@MovieViewModel.trailers.value = localTrailers + remoteTrailers
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -192,6 +218,7 @@ fun MovieDetails(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: MovieViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(destination.itemId, destination.item)
|
||||
}
|
||||
|
|
@ -291,13 +318,21 @@ fun MovieDetails(
|
|||
viewModel.setWatched((movie.data.userData?.played ?: false).not())
|
||||
},
|
||||
trailerOnClick = { trailer ->
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
itemId = trailer.id,
|
||||
item = trailer,
|
||||
positionMs = 0L,
|
||||
),
|
||||
)
|
||||
when (trailer) {
|
||||
is LocalTrailer ->
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
itemId = trailer.baseItem.id,
|
||||
item = trailer.baseItem,
|
||||
positionMs = 0L,
|
||||
),
|
||||
)
|
||||
|
||||
is RemoteTrailer -> {
|
||||
val intent = Intent(Intent.ACTION_VIEW, trailer.url.toUri())
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -339,9 +374,9 @@ fun MovieDetailsContent(
|
|||
chosenStreams: ChosenStreams?,
|
||||
people: List<Person>,
|
||||
chapters: List<Chapter>,
|
||||
trailers: List<BaseItem>,
|
||||
trailers: List<Trailer>,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
trailerOnClick: (BaseItem) -> Unit,
|
||||
trailerOnClick: (Trailer) -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
|
|
@ -440,22 +475,10 @@ fun MovieDetailsContent(
|
|||
}
|
||||
if (trailers.isNotEmpty()) {
|
||||
item {
|
||||
ItemRow(
|
||||
title = stringResource(R.string.trailers),
|
||||
items = trailers,
|
||||
onClickItem = trailerOnClick,
|
||||
onLongClickItem = {},
|
||||
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
|
||||
SeasonCard(
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
imageHeight = Cards.height2x3,
|
||||
imageWidth = Dp.Unspecified,
|
||||
showImageOverlay = false,
|
||||
modifier = mod,
|
||||
)
|
||||
},
|
||||
TrailerRow(
|
||||
trailers = trailers,
|
||||
onClickTrailer = trailerOnClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -472,3 +495,77 @@ fun MovieDetailsContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrailerRow(
|
||||
trailers: List<Trailer>,
|
||||
onClickTrailer: (Trailer) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.trailers),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
LazyRow(
|
||||
state = state,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRestorer(firstFocus),
|
||||
) {
|
||||
itemsIndexed(trailers) { index, item ->
|
||||
val cardModifier =
|
||||
if (index == 0) {
|
||||
Modifier.focusRequester(firstFocus)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
when (item) {
|
||||
is LocalTrailer ->
|
||||
SeasonCard(
|
||||
item = item.baseItem,
|
||||
onClick = { onClickTrailer.invoke(item) },
|
||||
onLongClick = {},
|
||||
imageHeight = Cards.height2x3,
|
||||
imageWidth = Dp.Unspecified,
|
||||
showImageOverlay = false,
|
||||
modifier = cardModifier,
|
||||
)
|
||||
|
||||
is RemoteTrailer -> {
|
||||
val subtitle =
|
||||
when (item.url.toUri().host) {
|
||||
"youtube.com", "www.youtube.com" -> "YouTube"
|
||||
else -> null
|
||||
}
|
||||
SeasonCard(
|
||||
title = item.name,
|
||||
subtitle = subtitle,
|
||||
name = item.name,
|
||||
imageUrl = null,
|
||||
isFavorite = false,
|
||||
isPlayed = false,
|
||||
unplayedItemCount = 0,
|
||||
playedPercentage = 0.0,
|
||||
onClick = { onClickTrailer.invoke(item) },
|
||||
onLongClick = {},
|
||||
modifier = cardModifier,
|
||||
showImageOverlay = false,
|
||||
imageHeight = Cards.height2x3,
|
||||
imageWidth = Dp.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import com.github.damontecres.wholphin.util.LoadingState
|
|||
import com.github.damontecres.wholphin.util.Release
|
||||
import com.github.damontecres.wholphin.util.UpdateChecker
|
||||
import com.github.damontecres.wholphin.util.Version
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -120,27 +121,21 @@ fun InstallUpdatePage(
|
|||
|
||||
LoadingState.Success ->
|
||||
release?.let {
|
||||
if (it.version.isGreaterThan(viewModel.currentVersion)) {
|
||||
InstallUpdatePageContent(
|
||||
currentVersion = viewModel.currentVersion,
|
||||
release = it,
|
||||
onInstallRelease = {
|
||||
if (!permissions) {
|
||||
launcher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
viewModel.installRelease(it)
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
viewModel.navigationManager.goBack()
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "No update available",
|
||||
)
|
||||
}
|
||||
InstallUpdatePageContent(
|
||||
currentVersion = viewModel.currentVersion,
|
||||
release = it,
|
||||
onInstallRelease = {
|
||||
if (!permissions) {
|
||||
launcher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
viewModel.installRelease(it)
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
viewModel.navigationManager.goBack()
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -202,11 +197,14 @@ fun InstallUpdatePageContent(
|
|||
},
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
// TODO render markdown
|
||||
text = release.notes.joinToString("\n\n") + (release.body ?: ""),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
Markdown(
|
||||
(release.notes.joinToString("\n\n") + (release.body ?: ""))
|
||||
.replace(
|
||||
Regex("https://github.com/damontecres/\\w+/pull/(\\d+)"),
|
||||
"#$1",
|
||||
)
|
||||
// Remove the last line for full changelog since its just a link
|
||||
.replace(Regex("\\*\\*Full Changelog\\*\\*.*"), ""),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue