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