mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Merge branch 'main' into develop/mpv
This commit is contained in:
commit
e3efb2e500
23 changed files with 166 additions and 97 deletions
|
|
@ -5,13 +5,13 @@ import android.content.SharedPreferences
|
|||
import androidx.core.content.edit
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.map
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.EqualityMutableLiveData
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -42,7 +42,7 @@ class ServerRepository
|
|||
) {
|
||||
private val sharedPreferences = getServerSharedPreferences(context)
|
||||
|
||||
private var _current = MutableLiveData<CurrentUser?>(null)
|
||||
private var _current = EqualityMutableLiveData<CurrentUser?>(null)
|
||||
val current: LiveData<CurrentUser?> = _current
|
||||
|
||||
val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server }
|
||||
|
|
|
|||
|
|
@ -74,6 +74,12 @@ object Cards {
|
|||
val playedPercentHeight = 6.dp
|
||||
}
|
||||
|
||||
object AspectRatios {
|
||||
const val WIDE = 16f / 9f
|
||||
const val FOUR_THREE = 4f / 3f
|
||||
const val TALL = 2f / 3f
|
||||
}
|
||||
|
||||
@Preview(
|
||||
device = "spec:parent=tv_1080p",
|
||||
backgroundColor = 0xFF383535,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.tv.material3.Text
|
|||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
|
|
@ -53,7 +54,7 @@ fun BannerCard(
|
|||
favorite: Boolean = false,
|
||||
playPercent: Double = 0.0,
|
||||
cardHeight: Dp = 140.dp * .85f,
|
||||
aspectRatio: Float = 16f / 9,
|
||||
aspectRatio: Float = AspectRatios.WIDE,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
var imageError by remember { mutableStateOf(false) }
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ fun ChapterCard(
|
|||
position: Duration,
|
||||
imageUrl: String?,
|
||||
onClick: () -> Unit,
|
||||
aspectRatio: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
cardHeight: Dp = 140.dp * .85f,
|
||||
aspectRatio: Float = 16f / 9,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.github.damontecres.wholphin.data.model.Chapter
|
|||
@Composable
|
||||
fun ChapterRow(
|
||||
chapters: List<Chapter>,
|
||||
aspectRatio: Float,
|
||||
onClick: (Chapter) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: ((Chapter) -> Unit)? = null,
|
||||
|
|
@ -50,6 +51,7 @@ fun ChapterRow(
|
|||
position = item.position,
|
||||
imageUrl = item.imageUrl,
|
||||
onClick = { onClick(item) },
|
||||
aspectRatio = aspectRatio,
|
||||
modifier = Modifier,
|
||||
onLongClick = onLongClick?.let { { it(item) } },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import androidx.tv.material3.CardDefaults
|
|||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -63,7 +64,7 @@ fun EpisodeCard(
|
|||
} else {
|
||||
focusedAfterDelay = false
|
||||
}
|
||||
val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
|
||||
val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL
|
||||
val width = imageHeight * aspectRatio
|
||||
val height = imageWidth * (1f / aspectRatio)
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.tv.material3.CardDefaults
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ fun GridCard(
|
|||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
imageAspectRatio: Float = 2f / 3f,
|
||||
imageAspectRatio: Float = AspectRatios.TALL,
|
||||
) {
|
||||
val dto = item?.data
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.util.FocusPair
|
||||
|
||||
@Composable
|
||||
|
|
@ -112,7 +113,8 @@ fun BannerItemRow(
|
|||
name = title,
|
||||
imageUrl = item?.imageUrl,
|
||||
aspectRatio =
|
||||
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
|
||||
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat()
|
||||
?: AspectRatios.WIDE,
|
||||
cornerText = item?.data?.indexNumber?.let { "E$it" },
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
playPercent = item?.data?.userData?.playedPercentage ?: 0.0,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.tv.material3.Card
|
|||
import androidx.tv.material3.CardDefaults
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ fun PersonCard(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(2f / 3f), // TODO,
|
||||
.aspectRatio(AspectRatios.TALL), // TODO,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.tv.material3.Card
|
|||
import androidx.tv.material3.CardDefaults
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ fun SeasonCard(
|
|||
imageWidth: Dp = Dp.Unspecified,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
showImageOverlay: Boolean = false,
|
||||
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f),
|
||||
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL,
|
||||
) = SeasonCard(
|
||||
title = item?.title,
|
||||
subtitle = item?.subtitle,
|
||||
|
|
@ -82,7 +83,7 @@ fun SeasonCard(
|
|||
imageWidth: Dp = Dp.Unspecified,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
showImageOverlay: Boolean = false,
|
||||
aspectRatio: Float = (2f / 3f),
|
||||
aspectRatio: Float = AspectRatios.TALL,
|
||||
) {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
||||
|
|
@ -102,7 +103,6 @@ fun SeasonCard(
|
|||
} else {
|
||||
focusedAfterDelay = false
|
||||
}
|
||||
// val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
|
||||
val width = imageHeight * aspectRatio
|
||||
val height = imageWidth * (1f / aspectRatio)
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
|||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
|
|
@ -26,6 +26,8 @@ import dagger.assisted.AssistedInject
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -95,8 +97,8 @@ class RecommendedMovieViewModel
|
|||
),
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
loading.value = LoadingState.Success
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching movie recommendations")
|
||||
|
|
@ -159,23 +161,31 @@ class RecommendedMovieViewModel
|
|||
R.string.suggestions to suggestedItems,
|
||||
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||
)
|
||||
|
||||
rows.forEachIndexed { index, (title, pager) ->
|
||||
viewModelScope.launchIO {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
if (pager.isNotEmpty()) {
|
||||
pager.getBlocking(0)
|
||||
rows
|
||||
.mapIndexed { index, (title, pager) ->
|
||||
viewModelScope.async(Dispatchers.IO + ExceptionHandler()) {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
if (pager.isNotEmpty()) {
|
||||
pager.getBlocking(0)
|
||||
}
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
}
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
update(index + 1, result)
|
||||
if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() &&
|
||||
(loading.value == LoadingState.Loading || loading.value == LoadingState.Pending)
|
||||
) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
update(index + 1, result)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
|||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
|
|
@ -27,6 +27,8 @@ import dagger.assisted.AssistedInject
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -123,8 +125,8 @@ class RecommendedTvShowViewModel
|
|||
),
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
loading.value = LoadingState.Success
|
||||
if (resumeItems.isNotEmpty() || nextUpItems.isNotEmpty()) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching tv recommendations")
|
||||
|
|
@ -188,19 +190,31 @@ class RecommendedTvShowViewModel
|
|||
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||
)
|
||||
|
||||
rows.forEachIndexed { index, (title, pager) ->
|
||||
viewModelScope.launchIO {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
rows
|
||||
.mapIndexed { index, (title, pager) ->
|
||||
viewModelScope.async(Dispatchers.IO + ExceptionHandler()) {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
if (pager.isNotEmpty()) {
|
||||
pager.getBlocking(0)
|
||||
}
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
}
|
||||
update(index + 2, result)
|
||||
if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() &&
|
||||
(loading.value == LoadingState.Loading || loading.value == LoadingState.Pending)
|
||||
) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
update(index + 2, result)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,11 @@ import com.github.damontecres.wholphin.data.model.LocalTrailer
|
|||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
|
|
@ -486,6 +488,7 @@ fun MovieDetailsContent(
|
|||
item {
|
||||
ChapterRow(
|
||||
chapters = chapters,
|
||||
aspectRatio = movie.data.aspectRatioFloat ?: AspectRatios.WIDE,
|
||||
onClick = {
|
||||
position = CHAPTER_ROW
|
||||
playOnClick.invoke(it.position)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ import coil3.compose.AsyncImage
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -216,10 +216,8 @@ fun SeriesOverviewContent(
|
|||
?.data
|
||||
?.primaryImageAspectRatio
|
||||
?.toFloat()
|
||||
?.coerceAtLeast(
|
||||
episode.data.aspectRatioFloat ?: (4f / 3f),
|
||||
)
|
||||
?: (16f / 9), // TODO some episode images don't match the file's aspect ratio
|
||||
?.coerceAtLeast(AspectRatios.FOUR_THREE)
|
||||
?: (AspectRatios.WIDE),
|
||||
cornerText = cornerText,
|
||||
played = episode?.data?.userData?.played ?: false,
|
||||
playPercent =
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import coil3.compose.AsyncImage
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
|
|
@ -197,8 +198,20 @@ fun HomePageContent(
|
|||
loadingState: LoadingState? = null,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val firstRow =
|
||||
remember {
|
||||
homeRows
|
||||
.indexOfFirst {
|
||||
when (it) {
|
||||
is HomeRowLoadingState.Error -> false
|
||||
is HomeRowLoadingState.Loading -> true
|
||||
is HomeRowLoadingState.Pending -> true
|
||||
is HomeRowLoadingState.Success -> it.items.isNotEmpty()
|
||||
}
|
||||
}.coerceAtLeast(0)
|
||||
}
|
||||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||
mutableStateOf(RowColumn(0, 0))
|
||||
mutableStateOf(RowColumn(firstRow, 0))
|
||||
}
|
||||
var focusedItem =
|
||||
position.let {
|
||||
|
|
@ -208,11 +221,19 @@ fun HomePageContent(
|
|||
val listState = rememberLazyListState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val positionFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
positionFocusRequester.tryRequestFocus()
|
||||
// Hacky, but mostly works
|
||||
delay(50)
|
||||
listState.animateScrollToItem(position.row)
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(homeRows) {
|
||||
if (!focused) {
|
||||
homeRows
|
||||
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
||||
.takeIf { it >= 0 }
|
||||
?.let {
|
||||
positionFocusRequester.tryRequestFocus()
|
||||
delay(50)
|
||||
listState.animateScrollToItem(position.row)
|
||||
focused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(position) {
|
||||
listState.animateScrollToItem(position.row)
|
||||
|
|
@ -333,11 +354,10 @@ fun HomePageContent(
|
|||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
// TODO better aspect ration handling?
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
imageUrl = item?.imageUrl,
|
||||
aspectRatio = (2f / 3f),
|
||||
aspectRatio = AspectRatios.TALL,
|
||||
cornerText =
|
||||
item?.data?.indexNumber?.let { "E$it" }
|
||||
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import coil3.compose.AsyncImagePreviewHandler
|
|||
import coil3.compose.LocalAsyncImagePreviewHandler
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -62,7 +63,7 @@ fun NextUpEpisode(
|
|||
onClick: () -> Unit,
|
||||
timeLeft: Duration?,
|
||||
modifier: Modifier = Modifier,
|
||||
aspectRatio: Float = 16f / 9,
|
||||
aspectRatio: Float = AspectRatios.WIDE,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
|
@ -202,7 +203,7 @@ private fun NextUpEpisodePreview() {
|
|||
"But it just keeps going and going.",
|
||||
imageUrl = "",
|
||||
onClick = {},
|
||||
aspectRatio = 4f / 3,
|
||||
aspectRatio = AspectRatios.FOUR_THREE,
|
||||
timeLeft = 30.seconds,
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.data.model.Chapter
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Playlist
|
||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterCard
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
|
|
@ -75,6 +77,7 @@ private val subtitleTextSize = 18.sp
|
|||
*/
|
||||
@Composable
|
||||
fun PlaybackOverlay(
|
||||
item: BaseItem?,
|
||||
title: String?,
|
||||
subtitleStreams: List<SubtitleStream>,
|
||||
chapters: List<Chapter>,
|
||||
|
|
@ -236,6 +239,7 @@ fun PlaybackOverlay(
|
|||
name = chapter.name,
|
||||
position = chapter.position,
|
||||
imageUrl = chapter.imageUrl,
|
||||
aspectRatio = item?.data?.aspectRatioFloat ?: AspectRatios.WIDE,
|
||||
onClick = {
|
||||
playerControls.seekTo(chapter.position.inWholeMilliseconds)
|
||||
controllerViewState.hideControls()
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ import com.github.damontecres.wholphin.data.model.Playlist
|
|||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
|
|
@ -332,6 +333,7 @@ fun PlaybackPage(
|
|||
.padding(WindowInsets.systemBars.asPaddingValues())
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent),
|
||||
item = currentPlayback?.item,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
subtitleStreams = subtitleStreams,
|
||||
|
|
@ -488,7 +490,9 @@ fun PlaybackPage(
|
|||
).joinToString(" - "),
|
||||
description = it.data.overview,
|
||||
imageUrl = it.imageUrl,
|
||||
aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
|
||||
aspectRatio =
|
||||
it.data.primaryImageAspectRatio?.toFloat()
|
||||
?: AspectRatios.WIDE,
|
||||
onClick = {
|
||||
viewModel.reportInteraction()
|
||||
viewModel.playNextUp()
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ import org.jellyfin.sdk.api.client.extensions.trickplayApi
|
|||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||
import org.jellyfin.sdk.api.sockets.subscribe
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||
|
|
@ -142,7 +141,7 @@ class PlaybackViewModel
|
|||
private lateinit var preferences: UserPreferences
|
||||
private lateinit var deviceProfile: DeviceProfile
|
||||
private lateinit var itemId: UUID
|
||||
private lateinit var dto: BaseItemDto
|
||||
private lateinit var item: BaseItem
|
||||
private var activityListener: TrackActivityPlaybackListener? = null
|
||||
|
||||
val nextUp = MutableLiveData<BaseItem?>()
|
||||
|
|
@ -204,10 +203,11 @@ class PlaybackViewModel
|
|||
} else {
|
||||
queriedItem
|
||||
}
|
||||
val item = BaseItem.from(base, api)
|
||||
|
||||
val played =
|
||||
play(
|
||||
base,
|
||||
item,
|
||||
destination.positionMs,
|
||||
destination.itemPlayback,
|
||||
destination.forceTranscoding,
|
||||
|
|
@ -228,25 +228,25 @@ class PlaybackViewModel
|
|||
}
|
||||
|
||||
private suspend fun play(
|
||||
base: BaseItemDto,
|
||||
item: BaseItem,
|
||||
positionMs: Long,
|
||||
itemPlayback: ItemPlayback? = null,
|
||||
forceTranscoding: Boolean = false,
|
||||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.i("Playing ${base.id}")
|
||||
Timber.i("Playing ${item.id}")
|
||||
autoSkippedSegments.clear()
|
||||
if (base.type !in supportItemKinds) {
|
||||
if (item.type !in supportItemKinds) {
|
||||
showToast(
|
||||
context,
|
||||
"Unsupported type '${base.type}', skipping...",
|
||||
"Unsupported type '${item.type}', skipping...",
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
return@withContext false
|
||||
}
|
||||
val isLiveTv = base.type == BaseItemKind.TV_CHANNEL
|
||||
val isLiveTv = item.type == BaseItemKind.TV_CHANNEL
|
||||
|
||||
dto = base
|
||||
val base = item.data
|
||||
val title =
|
||||
if (base.type == BaseItemKind.EPISODE) {
|
||||
base.seriesName
|
||||
|
|
@ -358,7 +358,7 @@ class PlaybackViewModel
|
|||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
||||
|
||||
changeStreams(
|
||||
base,
|
||||
item,
|
||||
itemPlaybackToUse,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
|
|
@ -379,7 +379,7 @@ class PlaybackViewModel
|
|||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun changeStreams(
|
||||
item: BaseItemDto,
|
||||
item: BaseItem,
|
||||
currentItemPlayback: ItemPlayback = this@PlaybackViewModel.currentItemPlayback.value!!,
|
||||
audioIndex: Int?,
|
||||
subtitleIndex: Int?,
|
||||
|
|
@ -392,7 +392,7 @@ class PlaybackViewModel
|
|||
val playerBackend = preferences.appPreferences.playbackPreferences.playerBackend
|
||||
|
||||
val currentPlayback = this@PlaybackViewModel.currentPlayback.value
|
||||
if (currentPlayback != null && currentPlayback.itemId == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) {
|
||||
if (currentPlayback != null && currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) {
|
||||
// If direct playing, can try to switch tracks without playback restarting
|
||||
// Except for external subtitles
|
||||
// TODO there's probably no reason why we can't add external subtitles?
|
||||
|
|
@ -549,9 +549,9 @@ class PlaybackViewModel
|
|||
|
||||
val playback =
|
||||
CurrentPlayback(
|
||||
itemId = item.id,
|
||||
backend = preferences.appPreferences.playbackPreferences.playerBackend,
|
||||
item = item,
|
||||
tracks = listOf(),
|
||||
backend = preferences.appPreferences.playbackPreferences.playerBackend,
|
||||
playMethod = transcodeType,
|
||||
playSessionId = response.playSessionId,
|
||||
liveStreamId = source.liveStreamId,
|
||||
|
|
@ -623,7 +623,7 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
val trickPlayInfo =
|
||||
item.trickplay
|
||||
item.data.trickplay
|
||||
?.get(source.id)
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
|
|
@ -637,7 +637,7 @@ class PlaybackViewModel
|
|||
fun changeAudioStream(index: Int) {
|
||||
viewModelScope.launchIO {
|
||||
changeStreams(
|
||||
dto,
|
||||
item,
|
||||
currentItemPlayback.value!!,
|
||||
index,
|
||||
currentItemPlayback.value?.subtitleIndex,
|
||||
|
|
@ -650,7 +650,7 @@ class PlaybackViewModel
|
|||
fun changeSubtitleStream(index: Int?): Job =
|
||||
viewModelScope.launchIO {
|
||||
changeStreams(
|
||||
dto,
|
||||
item,
|
||||
currentItemPlayback.value!!,
|
||||
currentItemPlayback.value?.audioIndex,
|
||||
index,
|
||||
|
|
@ -660,7 +660,7 @@ class PlaybackViewModel
|
|||
}
|
||||
|
||||
fun getTrickplayUrl(index: Int): String? {
|
||||
val itemId = dto.id
|
||||
val itemId = item.id
|
||||
val mediaSourceId = currentItemPlayback.value?.sourceId
|
||||
val trickPlayInfo = trickplay.value ?: return null
|
||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||
|
|
@ -792,7 +792,7 @@ class PlaybackViewModel
|
|||
viewModelScope.launchIO {
|
||||
cancelUpNextEpisode()
|
||||
val item = it.getAndAdvance()
|
||||
val played = play(item.data, 0)
|
||||
val played = play(item, 0)
|
||||
if (!played) {
|
||||
playNextUp()
|
||||
}
|
||||
|
|
@ -807,7 +807,7 @@ class PlaybackViewModel
|
|||
viewModelScope.launchIO {
|
||||
cancelUpNextEpisode()
|
||||
val item = it.getPreviousAndReverse()
|
||||
val played = play(item.data, 0)
|
||||
val played = play(item, 0)
|
||||
if (!played) {
|
||||
playPrevious()
|
||||
}
|
||||
|
|
@ -825,7 +825,7 @@ class PlaybackViewModel
|
|||
viewModelScope.launchIO {
|
||||
val toPlay = playlist.advanceTo(item.id)
|
||||
if (toPlay != null) {
|
||||
val played = play(toPlay.data, 0)
|
||||
val played = play(toPlay, 0)
|
||||
if (!played) {
|
||||
playNextUp()
|
||||
}
|
||||
|
|
@ -859,7 +859,7 @@ class PlaybackViewModel
|
|||
PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> {
|
||||
Timber.w("Playback error during ${it.playMethod}, falling back to transcoding")
|
||||
changeStreams(
|
||||
dto,
|
||||
item,
|
||||
currentItemPlayback.value!!,
|
||||
currentItemPlayback.value?.audioIndex,
|
||||
currentItemPlayback.value?.subtitleIndex,
|
||||
|
|
@ -978,8 +978,12 @@ class PlaybackViewModel
|
|||
while (maxAttempts > 0 && subtitleCount == newCount) {
|
||||
maxAttempts--
|
||||
delay(1500)
|
||||
dto = api.userLibraryApi.getItem(itemId = dto.id).content
|
||||
val mediaSource = chooseSource(dto, it)
|
||||
item =
|
||||
BaseItem.from(
|
||||
api.userLibraryApi.getItem(itemId = item.id).content,
|
||||
api,
|
||||
)
|
||||
val mediaSource = chooseSource(item.data, it)
|
||||
if (mediaSource == null) {
|
||||
// This shouldn't happen, but just in case
|
||||
showToast(
|
||||
|
|
@ -1036,7 +1040,7 @@ class PlaybackViewModel
|
|||
audioIndex += 1
|
||||
}
|
||||
changeStreams(
|
||||
dto,
|
||||
item,
|
||||
currentItemPlayback.value!!,
|
||||
audioIndex,
|
||||
newStream.index,
|
||||
|
|
@ -1066,9 +1070,9 @@ class PlaybackViewModel
|
|||
}
|
||||
|
||||
data class CurrentPlayback(
|
||||
val itemId: UUID,
|
||||
val backend: PlayerBackend,
|
||||
val item: BaseItem,
|
||||
val tracks: List<TrackSupport>,
|
||||
val backend: PlayerBackend,
|
||||
val playMethod: PlayMethod,
|
||||
val playSessionId: String?,
|
||||
val liveStreamId: String?,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ fun SwitchServerContent(
|
|||
var showAddServer by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init()
|
||||
viewModel.discoverServers()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,10 +52,6 @@ class SwitchServerViewModel
|
|||
addServerState.value = LoadingState.Pending
|
||||
}
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launchIO {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ fun SwitchUserContent(
|
|||
),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init()
|
||||
}
|
||||
|
||||
// val currentServer by viewModel.serverRepository.currentServer.observeAsState()
|
||||
val currentUser by viewModel.serverRepository.currentUser.observeAsState()
|
||||
|
|
|
|||
|
|
@ -48,12 +48,6 @@ class SwitchUserViewModel
|
|||
fun create(server: JellyfinServer): SwitchUserViewModel
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
|
||||
serverRepository.switchServerOrUser()
|
||||
}
|
||||
}
|
||||
|
||||
val serverQuickConnect = MutableLiveData<Boolean>(false)
|
||||
|
||||
val users = MutableLiveData<List<JellyfinUser>>(listOf())
|
||||
|
|
@ -78,6 +72,9 @@ class SwitchUserViewModel
|
|||
}
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
|
||||
serverRepository.switchServerOrUser()
|
||||
}
|
||||
quickConnectJob?.cancel()
|
||||
viewModelScope.launchIO {
|
||||
users.setValueOnMain(listOf())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue