mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Lots of UI changes
This commit is contained in:
parent
6e185b25b7
commit
379559c076
16 changed files with 557 additions and 71 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
package com.github.damontecres.dolphin
|
package com.github.damontecres.dolphin
|
||||||
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import androidx.compose.foundation.MarqueeAnimationMode
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -10,6 +12,7 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
|
@ -108,3 +111,14 @@ fun OneTimeLaunchedEffect(runOnceBlock: suspend CoroutineScope.() -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Modifier.enableMarquee(focused: Boolean) =
|
||||||
|
if (focused) {
|
||||||
|
basicMarquee(
|
||||||
|
initialDelayMillis = 250,
|
||||||
|
animationMode = MarqueeAnimationMode.Immediately,
|
||||||
|
velocity = 40.dp,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.github.damontecres.dolphin.data.model
|
||||||
|
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BaseItem(
|
||||||
|
val data: BaseItemDto,
|
||||||
|
val imageUrl: String?,
|
||||||
|
) {
|
||||||
|
@Transient val id = data.id
|
||||||
|
|
||||||
|
@Transient val type = data.type
|
||||||
|
|
||||||
|
@Transient val name = data.name
|
||||||
|
|
||||||
|
fun destination() = Destination.MediaItem(id, type, this)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(
|
||||||
|
dto: BaseItemDto,
|
||||||
|
api: ApiClient,
|
||||||
|
) = BaseItem(dto, api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import com.github.damontecres.dolphin.R
|
import com.github.damontecres.dolphin.R
|
||||||
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
|
||||||
val FontAwesome = FontFamily(Font(resId = R.font.fa_solid_900))
|
val FontAwesome = FontFamily(Font(resId = R.font.fa_solid_900))
|
||||||
|
|
||||||
|
|
@ -14,3 +15,12 @@ sealed class AppColors private constructor() {
|
||||||
val TransparentBlack75 = Color(0xBF000000)
|
val TransparentBlack75 = Color(0xBF000000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val DEFAULT_PAGE_SIZE = 50
|
||||||
|
|
||||||
|
val DefaultItemFields =
|
||||||
|
listOf(
|
||||||
|
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||||
|
ItemFields.SEASON_USER_DATA,
|
||||||
|
ItemFields.CHILD_COUNT,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CardRow(
|
||||||
|
title: String,
|
||||||
|
items: List<BaseItem?>,
|
||||||
|
onClickItem: (BaseItem) -> Unit,
|
||||||
|
onLongClickItem: (BaseItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
items(items) { item ->
|
||||||
|
ItemCard(
|
||||||
|
item = item,
|
||||||
|
onClick = { if (item != null) onClickItem.invoke(item) },
|
||||||
|
onLongClick = { if (item != null) onLongClickItem.invoke(item) },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,55 +1,209 @@
|
||||||
package com.github.damontecres.dolphin.ui.cards
|
package com.github.damontecres.dolphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.Card
|
import androidx.tv.material3.Card
|
||||||
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import com.github.damontecres.dolphin.R
|
||||||
import java.util.UUID
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.enableMarquee
|
||||||
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemCard(
|
fun ItemCard(
|
||||||
item: BaseItemDto?,
|
item: BaseItem?,
|
||||||
imageUrlBuilder: (UUID) -> String?,
|
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
cardWidth: Dp = 150.dp,
|
cardWidth: Dp = 150.dp,
|
||||||
cardHeight: Dp = 200.dp,
|
cardHeight: Dp = 200.dp,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) {
|
) {
|
||||||
|
val hideOverlayDelay = 1_000L
|
||||||
|
|
||||||
|
val focused = interactionSource.collectIsFocusedAsState().value
|
||||||
|
var focusedAfterDelay by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (focused) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(hideOverlayDelay)
|
||||||
|
if (focused) {
|
||||||
|
focusedAfterDelay = true
|
||||||
|
} else {
|
||||||
|
focusedAfterDelay = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
focusedAfterDelay = false
|
||||||
|
}
|
||||||
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
NullCard(modifier, cardWidth, cardHeight)
|
NullCard(modifier, cardWidth, cardHeight)
|
||||||
} else {
|
} else {
|
||||||
|
val dto = item.data
|
||||||
|
// TODO better aspect ratio handling
|
||||||
|
val height =
|
||||||
|
if (dto.primaryImageAspectRatio != null && dto.primaryImageAspectRatio!! > 1) cardWidth else cardHeight
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.width(cardWidth),
|
modifier = Modifier.width(cardWidth),
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
ItemCardImage(
|
||||||
model = imageUrlBuilder.invoke(item.id),
|
imageUrl = item.imageUrl,
|
||||||
contentDescription = item.name,
|
name = item.name,
|
||||||
contentScale = ContentScale.Fit,
|
showOverlay = !focusedAfterDelay,
|
||||||
|
favorite = dto.userData?.isFavorite ?: false,
|
||||||
|
watched = dto.userData?.played ?: false,
|
||||||
|
unwatchedCount = dto.userData?.unplayedItemCount ?: -1,
|
||||||
|
watchedPercent = dto.userData?.playedPercentage,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(cardHeight),
|
.height(height),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = item.name ?: "",
|
text = item.name ?: "",
|
||||||
|
maxLines = 1,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.enableMarquee(focusedAfterDelay),
|
||||||
|
)
|
||||||
|
if (dto.type == BaseItemKind.EPISODE) {
|
||||||
|
if (dto.parentIndexNumber != null && dto.indexNumber != null) {
|
||||||
|
Text(
|
||||||
|
text = "S${dto.parentIndexNumber} E${dto.indexNumber}",
|
||||||
)
|
)
|
||||||
Text(text = item.primaryImageAspectRatio.toString())
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemCardImage(
|
||||||
|
imageUrl: String?,
|
||||||
|
name: String?,
|
||||||
|
showOverlay: Boolean,
|
||||||
|
favorite: Boolean,
|
||||||
|
watched: Boolean,
|
||||||
|
unwatchedCount: Int,
|
||||||
|
watchedPercent: Double?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = name,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showOverlay,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (favorite) {
|
||||||
|
Text(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(8.dp),
|
||||||
|
color = colorResource(android.R.color.holo_red_light),
|
||||||
|
text = stringResource(R.string.fa_heart),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (watched) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.border.copy(alpha = 1f),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.size(48.dp)
|
||||||
|
.padding(8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (unwatchedCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.border,
|
||||||
|
shape = CircleShape,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
text = unwatchedCount.toString(),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchedPercent?.let { percent ->
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.tertiary,
|
||||||
|
).clip(RectangleShape)
|
||||||
|
.height(4.dp)
|
||||||
|
.fillMaxWidth((percent / 100.0).toFloat()),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.R
|
import com.github.damontecres.dolphin.R
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.ifElse
|
import com.github.damontecres.dolphin.ifElse
|
||||||
import com.github.damontecres.dolphin.tryRequestFocus
|
import com.github.damontecres.dolphin.tryRequestFocus
|
||||||
import com.github.damontecres.dolphin.ui.AppColors
|
import com.github.damontecres.dolphin.ui.AppColors
|
||||||
|
|
@ -52,21 +53,16 @@ import com.github.damontecres.dolphin.ui.playback.isForwardButton
|
||||||
import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp
|
import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp
|
||||||
import com.github.damontecres.dolphin.util.DolphinPager
|
import com.github.damontecres.dolphin.util.DolphinPager
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
private val DEBUG = false
|
private const val DEBUG = false
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CardGrid(
|
fun CardGrid(
|
||||||
api: ApiClient,
|
|
||||||
pager: DolphinPager,
|
pager: DolphinPager,
|
||||||
itemOnClick: (BaseItemDto) -> Unit,
|
itemOnClick: (BaseItem) -> Unit,
|
||||||
longClicker: (BaseItemDto) -> Unit,
|
longClicker: (BaseItem) -> Unit,
|
||||||
letterPosition: suspend (Char) -> Int,
|
letterPosition: suspend (Char) -> Int,
|
||||||
requestFocus: Boolean,
|
requestFocus: Boolean,
|
||||||
gridFocusRequester: FocusRequester,
|
gridFocusRequester: FocusRequester,
|
||||||
|
|
@ -329,7 +325,6 @@ fun CardGrid(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { if (item != null) itemOnClick.invoke(item) },
|
onClick = { if (item != null) itemOnClick.invoke(item) },
|
||||||
onLongClick = { if (item != null) longClicker.invoke(item) },
|
onLongClick = { if (item != null) longClicker.invoke(item) },
|
||||||
imageUrlBuilder = { api.imageApi.getItemImageUrl(it, ImageType.PRIMARY) },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.OneTimeLaunchedEffect
|
import com.github.damontecres.dolphin.OneTimeLaunchedEffect
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.data.model.Library
|
import com.github.damontecres.dolphin.data.model.Library
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
|
@ -20,7 +21,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
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.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
@ -41,7 +41,7 @@ class CollectionFolderViewModel
|
||||||
|
|
||||||
override fun init(
|
override fun init(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
potential: BaseItemDto?,
|
potential: BaseItem?,
|
||||||
): Job =
|
): Job =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
super.init(itemId, potential)?.join()
|
super.init(itemId, potential)?.join()
|
||||||
|
|
@ -93,7 +93,14 @@ fun CollectionFolderDetails(
|
||||||
CollectionType.UNKNOWN -> TODO()
|
CollectionType.UNKNOWN -> TODO()
|
||||||
CollectionType.MOVIES -> TODO()
|
CollectionType.MOVIES -> TODO()
|
||||||
CollectionType.TVSHOWS -> {
|
CollectionType.TVSHOWS -> {
|
||||||
TVShowCollectionDetails(viewModel.api, preferences, navigationManager, library!!, item!!, pager, modifier)
|
TVShowCollectionDetails(
|
||||||
|
preferences,
|
||||||
|
navigationManager,
|
||||||
|
library!!,
|
||||||
|
item!!,
|
||||||
|
pager,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionType.MUSIC -> TODO()
|
CollectionType.MUSIC -> TODO()
|
||||||
|
|
@ -113,17 +120,15 @@ fun CollectionFolderDetails(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TVShowCollectionDetails(
|
fun TVShowCollectionDetails(
|
||||||
api: ApiClient,
|
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
navigationManager: NavigationManager,
|
navigationManager: NavigationManager,
|
||||||
library: Library,
|
library: Library,
|
||||||
item: BaseItemDto,
|
item: BaseItem,
|
||||||
pager: DolphinPager,
|
pager: DolphinPager,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val gridFocusRequester = remember { FocusRequester() }
|
val gridFocusRequester = remember { FocusRequester() }
|
||||||
CardGrid(
|
CardGrid(
|
||||||
api = api,
|
|
||||||
pager = pager,
|
pager = pager,
|
||||||
itemOnClick = { navigationManager.navigateTo(Destination.MediaItem(it.id, it.type, it)) },
|
itemOnClick = { navigationManager.navigateTo(Destination.MediaItem(it.id, it.type, it)) },
|
||||||
longClicker = {},
|
longClicker = {},
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package com.github.damontecres.dolphin.ui.detail
|
package com.github.damontecres.dolphin.ui.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
|
@ -40,10 +43,27 @@ fun EpisodeDetails(
|
||||||
Text(text = "Loading...")
|
Text(text = "Loading...")
|
||||||
} else {
|
} else {
|
||||||
item?.let { item ->
|
item?.let { item ->
|
||||||
LazyColumn(modifier = modifier) {
|
val dto = item.data
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
contentPadding = PaddingValues(32.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
item {
|
item {
|
||||||
Text(text = item.name ?: "Unknown")
|
Text(text = item.name ?: "Unknown")
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
if (dto.parentIndexNumber != null && dto.indexNumber != null) {
|
||||||
|
Text(
|
||||||
|
text = "S${dto.parentIndexNumber} E${dto.indexNumber}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dto.overview?.let {
|
||||||
|
item {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,25 @@ package com.github.damontecres.dolphin.ui.detail
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.data.model.DolphinModel
|
import com.github.damontecres.dolphin.data.model.DolphinModel
|
||||||
import com.github.damontecres.dolphin.data.model.convertModel
|
import com.github.damontecres.dolphin.data.model.convertModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class ItemViewModel<T : DolphinModel>(
|
abstract class ItemViewModel<T : DolphinModel>(
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val item = MutableLiveData<BaseItemDto?>(null)
|
val item = MutableLiveData<BaseItem?>(null)
|
||||||
val model = MutableLiveData<T?>(null)
|
val model = MutableLiveData<T?>(null)
|
||||||
|
|
||||||
open fun init(
|
open fun init(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
potential: BaseItemDto?,
|
potential: BaseItem?,
|
||||||
): Job? {
|
): Job? {
|
||||||
if (item.value == null && potential?.id == itemId) {
|
if (item.value == null && potential?.id == itemId) {
|
||||||
item.value = potential
|
item.value = potential
|
||||||
|
|
@ -34,7 +34,7 @@ abstract class ItemViewModel<T : DolphinModel>(
|
||||||
try {
|
try {
|
||||||
val fetchedItem = api.userLibraryApi.getItem(itemId).content
|
val fetchedItem = api.userLibraryApi.getItem(itemId).content
|
||||||
val modelInstance = convertModel(fetchedItem, api)
|
val modelInstance = convertModel(fetchedItem, api)
|
||||||
item.value = fetchedItem
|
item.value = BaseItem.from(fetchedItem, api)
|
||||||
model.value = modelInstance as T
|
model.value = modelInstance as T
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to load item $itemId")
|
Timber.e(e, "Failed to load item $itemId")
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ fun MediaItemContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (destination.type) {
|
when (destination.type) {
|
||||||
BaseItemKind.SERIES -> TODO()
|
BaseItemKind.SERIES -> SeriesDetails(preferences, navigationManager, destination, modifier)
|
||||||
BaseItemKind.SEASON -> TODO()
|
BaseItemKind.SEASON -> SeasonDetails(preferences, navigationManager, destination, modifier)
|
||||||
BaseItemKind.EPISODE -> EpisodeDetails(preferences, navigationManager, destination, modifier)
|
BaseItemKind.EPISODE -> EpisodeDetails(preferences, navigationManager, destination, modifier)
|
||||||
BaseItemKind.MOVIE -> TODO()
|
BaseItemKind.MOVIE -> TODO()
|
||||||
BaseItemKind.VIDEO -> TODO()
|
BaseItemKind.VIDEO -> TODO()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.data.model.Video
|
||||||
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.dolphin.ui.cards.CardRow
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.dolphin.util.DolphinPager
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SeasonViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
api: ApiClient,
|
||||||
|
) : ItemViewModel<Video>(api) {
|
||||||
|
val episodes = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
||||||
|
override fun init(
|
||||||
|
itemId: UUID,
|
||||||
|
potential: BaseItem?,
|
||||||
|
): Job? =
|
||||||
|
viewModelScope.launch {
|
||||||
|
super.init(itemId, potential)?.join()
|
||||||
|
item.value?.let { item ->
|
||||||
|
val request =
|
||||||
|
GetItemsRequest(
|
||||||
|
parentId = item.id,
|
||||||
|
recursive = false,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||||
|
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
||||||
|
sortOrder = listOf(SortOrder.ASCENDING),
|
||||||
|
fields = listOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, ItemFields.CHILD_COUNT),
|
||||||
|
)
|
||||||
|
val pager = DolphinPager(api, request, viewModelScope, itemCount = item.data.childCount)
|
||||||
|
pager.init()
|
||||||
|
episodes.value = pager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SeasonDetails(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
navigationManager: NavigationManager,
|
||||||
|
destination: Destination.MediaItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: SeasonViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(destination.itemId, destination.item)
|
||||||
|
}
|
||||||
|
val item by viewModel.item.observeAsState()
|
||||||
|
val episodes by viewModel.episodes.observeAsState(listOf())
|
||||||
|
if (item == null) {
|
||||||
|
Text(text = "Loading...")
|
||||||
|
} else {
|
||||||
|
item?.let { item ->
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
item {
|
||||||
|
Text(text = item.name ?: "Unknown")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
CardRow(
|
||||||
|
title = "Episodes",
|
||||||
|
items = episodes,
|
||||||
|
onClickItem = { navigationManager.navigateTo(it.destination()) },
|
||||||
|
onLongClickItem = { },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.data.model.Video
|
||||||
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.dolphin.ui.cards.CardRow
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.dolphin.util.DolphinPager
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SeriesViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
api: ApiClient,
|
||||||
|
) : ItemViewModel<Video>(api) {
|
||||||
|
val seasons = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
||||||
|
override fun init(
|
||||||
|
itemId: UUID,
|
||||||
|
potential: BaseItem?,
|
||||||
|
): Job? =
|
||||||
|
viewModelScope.launch {
|
||||||
|
super.init(itemId, potential)?.join()
|
||||||
|
item.value?.let { item ->
|
||||||
|
val request =
|
||||||
|
GetItemsRequest(
|
||||||
|
parentId = item.id,
|
||||||
|
recursive = false,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.SEASON),
|
||||||
|
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
||||||
|
sortOrder = listOf(SortOrder.ASCENDING),
|
||||||
|
fields =
|
||||||
|
listOf(
|
||||||
|
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||||
|
ItemFields.CHILD_COUNT,
|
||||||
|
ItemFields.SEASON_USER_DATA,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val pager = DolphinPager(api, request, viewModelScope, itemCount = item.data.childCount)
|
||||||
|
pager.init()
|
||||||
|
seasons.value = pager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SeriesDetails(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
navigationManager: NavigationManager,
|
||||||
|
destination: Destination.MediaItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(destination.itemId, destination.item)
|
||||||
|
}
|
||||||
|
val item by viewModel.item.observeAsState()
|
||||||
|
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||||
|
if (item == null) {
|
||||||
|
Text(text = "Loading...")
|
||||||
|
} else {
|
||||||
|
item?.let { item ->
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
item {
|
||||||
|
Text(text = item.name ?: "Unknown")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
CardRow(
|
||||||
|
title = "Seasons",
|
||||||
|
items = seasons,
|
||||||
|
onClickItem = { navigationManager.navigateTo(it.destination()) },
|
||||||
|
onLongClickItem = { },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,11 +19,11 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.data.model.DolphinModel
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.data.model.convertModel
|
|
||||||
import com.github.damontecres.dolphin.isNotNullOrBlank
|
import com.github.damontecres.dolphin.isNotNullOrBlank
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.cards.DolphinCard
|
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||||
|
import com.github.damontecres.dolphin.ui.cards.ItemCard
|
||||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -44,7 +44,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
data class HomeRow(
|
data class HomeRow(
|
||||||
val section: HomeSection,
|
val section: HomeSection,
|
||||||
val items: List<DolphinModel>,
|
val items: List<BaseItem>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -80,7 +80,7 @@ class MainViewModel
|
||||||
user.configuration?.orderedViews?.firstOrNull()?.let { viewId ->
|
user.configuration?.orderedViews?.firstOrNull()?.let { viewId ->
|
||||||
val request =
|
val request =
|
||||||
GetLatestMediaRequest(
|
GetLatestMediaRequest(
|
||||||
fields = listOf(),
|
fields = DefaultItemFields,
|
||||||
imageTypeLimit = 1,
|
imageTypeLimit = 1,
|
||||||
parentId = viewId,
|
parentId = viewId,
|
||||||
groupItems = true,
|
groupItems = true,
|
||||||
|
|
@ -90,7 +90,7 @@ class MainViewModel
|
||||||
api.userLibraryApi
|
api.userLibraryApi
|
||||||
.getLatestMedia(request)
|
.getLatestMedia(request)
|
||||||
.content
|
.content
|
||||||
.map { convertModel(it, api) }
|
.map { BaseItem.from(it, api) }
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = section,
|
section = section,
|
||||||
items = latest,
|
items = latest,
|
||||||
|
|
@ -104,6 +104,7 @@ class MainViewModel
|
||||||
val request =
|
val request =
|
||||||
GetResumeItemsRequest(
|
GetResumeItemsRequest(
|
||||||
userId = user.id,
|
userId = user.id,
|
||||||
|
fields = DefaultItemFields,
|
||||||
// TODO, more params?
|
// TODO, more params?
|
||||||
)
|
)
|
||||||
val items =
|
val items =
|
||||||
|
|
@ -111,7 +112,7 @@ class MainViewModel
|
||||||
.getResumeItems(request)
|
.getResumeItems(request)
|
||||||
.content
|
.content
|
||||||
.items
|
.items
|
||||||
.map { convertModel(it, api) }
|
.map { BaseItem.from(it, api) }
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = section,
|
section = section,
|
||||||
items = items,
|
items = items,
|
||||||
|
|
@ -121,7 +122,7 @@ class MainViewModel
|
||||||
HomeSection.NEXT_UP -> {
|
HomeSection.NEXT_UP -> {
|
||||||
val request =
|
val request =
|
||||||
GetNextUpRequest(
|
GetNextUpRequest(
|
||||||
fields = listOf(),
|
fields = DefaultItemFields,
|
||||||
imageTypeLimit = 1,
|
imageTypeLimit = 1,
|
||||||
parentId = null,
|
parentId = null,
|
||||||
limit = 25,
|
limit = 25,
|
||||||
|
|
@ -132,7 +133,7 @@ class MainViewModel
|
||||||
.getNextUp(request)
|
.getNextUp(request)
|
||||||
.content
|
.content
|
||||||
.items
|
.items
|
||||||
.map { convertModel(it, api) }
|
.map { BaseItem.from(it, api) }
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = section,
|
section = section,
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
|
|
@ -177,6 +178,7 @@ fun MainPage(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onLongClickItem = {},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +190,8 @@ fun MainPage(
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePageRow(
|
fun HomePageRow(
|
||||||
row: HomeRow,
|
row: HomeRow,
|
||||||
onClickItem: (DolphinModel) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
|
onLongClickItem: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -207,9 +210,10 @@ fun HomePageRow(
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
items(row.items) { item ->
|
items(row.items) { item ->
|
||||||
DolphinCard(
|
ItemCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { onClickItem.invoke(item) },
|
onClick = { onClickItem.invoke(item) },
|
||||||
|
onLongClick = { onLongClickItem.invoke(item) },
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package com.github.damontecres.dolphin.ui.nav
|
package com.github.damontecres.dolphin.ui.nav
|
||||||
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.util.UuidSerializer
|
import com.github.damontecres.dolphin.util.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import kotlinx.serialization.Transient
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -27,13 +28,13 @@ sealed class Destination(
|
||||||
data class MediaItem(
|
data class MediaItem(
|
||||||
@Serializable(with = UuidSerializer::class) val itemId: UUID,
|
@Serializable(with = UuidSerializer::class) val itemId: UUID,
|
||||||
val type: BaseItemKind,
|
val type: BaseItemKind,
|
||||||
@Transient val item: BaseItemDto? = null,
|
@Transient val item: BaseItem? = null,
|
||||||
) : Destination()
|
) : Destination()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Playback(
|
data class Playback(
|
||||||
@Serializable(with = UuidSerializer::class) val itemId: UUID,
|
@Serializable(with = UuidSerializer::class) val itemId: UUID,
|
||||||
val positionMs: Long,
|
val positionMs: Long,
|
||||||
@Transient val item: BaseItemDto? = null,
|
@Transient val item: BaseItem? = null,
|
||||||
) : Destination(true)
|
) : Destination(true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
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
|
||||||
|
|
@ -15,7 +16,6 @@ import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||||
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.PlaybackInfoDto
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
@ -70,19 +70,20 @@ class PlaybackViewModel
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
item: BaseItemDto?,
|
item: BaseItem?,
|
||||||
) {
|
) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
title.value = item.name
|
title.value = item.name
|
||||||
|
val base = item.data
|
||||||
if (item.type == BaseItemKind.EPISODE) {
|
if (item.type == BaseItemKind.EPISODE) {
|
||||||
val season = item.parentIndexNumber?.toString()?.padStart(2, '0')
|
val season = base.parentIndexNumber?.toString()?.padStart(2, '0')
|
||||||
val episode = item.indexNumber?.toString()?.padStart(2, '0')
|
val episode = base.indexNumber?.toString()?.padStart(2, '0')
|
||||||
// TODO multi episode support
|
// TODO multi episode support
|
||||||
if (season != null && episode != null) {
|
if (season != null && episode != null) {
|
||||||
subtitle.value =
|
subtitle.value =
|
||||||
buildString {
|
buildString {
|
||||||
if (item.seriesName != null) {
|
if (base.seriesName != null) {
|
||||||
append(item.seriesName)
|
append(base.seriesName)
|
||||||
append(" - ")
|
append(" - ")
|
||||||
}
|
}
|
||||||
append("S${season}E$episode")
|
append("S${season}E$episode")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.ui.DEFAULT_PAGE_SIZE
|
||||||
import com.google.common.cache.CacheBuilder
|
import com.google.common.cache.CacheBuilder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -13,7 +15,6 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -21,19 +22,21 @@ class DolphinPager(
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val request: GetItemsRequest,
|
val request: GetItemsRequest,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val pageSize: Int = 25,
|
private val pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||||
|
itemCount: Int? = null,
|
||||||
cacheSize: Long = 8,
|
cacheSize: Long = 8,
|
||||||
) : AbstractList<BaseItemDto?>() {
|
) : AbstractList<BaseItem?>() {
|
||||||
private var items by mutableStateOf(ItemList<BaseItemDto>(0, pageSize, mapOf()))
|
private var items by mutableStateOf(ItemList<BaseItem>(0, pageSize, mapOf()))
|
||||||
private var totalCount by mutableIntStateOf(-1)
|
private var totalCount by mutableIntStateOf(itemCount ?: -1)
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val cachedPages =
|
private val cachedPages =
|
||||||
CacheBuilder
|
CacheBuilder
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.maximumSize(cacheSize)
|
.maximumSize(cacheSize)
|
||||||
.build<Int, List<BaseItemDto>>()
|
.build<Int, List<BaseItem>>()
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
|
if (totalCount < 0) {
|
||||||
val result =
|
val result =
|
||||||
api.itemsApi
|
api.itemsApi
|
||||||
.getItems(
|
.getItems(
|
||||||
|
|
@ -45,8 +48,9 @@ class DolphinPager(
|
||||||
).content
|
).content
|
||||||
totalCount = result.totalRecordCount
|
totalCount = result.totalRecordCount
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override operator fun get(index: Int): BaseItemDto? {
|
override operator fun get(index: Int): BaseItem? {
|
||||||
if (index in 0..<totalCount) {
|
if (index in 0..<totalCount) {
|
||||||
val item = items[index]
|
val item = items[index]
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
|
|
@ -58,7 +62,7 @@ class DolphinPager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getBlocking(position: Int): BaseItemDto? {
|
suspend fun getBlocking(position: Int): BaseItem? {
|
||||||
if (position in 0..<totalCount) {
|
if (position in 0..<totalCount) {
|
||||||
val item = items[position]
|
val item = items[position]
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
|
|
@ -89,7 +93,7 @@ class DolphinPager(
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
),
|
),
|
||||||
).content
|
).content
|
||||||
val data = result.items
|
val data = result.items.map { BaseItem.from(it, api) }
|
||||||
cachedPages.put(pageNumber, data)
|
cachedPages.put(pageNumber, data)
|
||||||
items = ItemList(totalCount, pageSize, cachedPages.asMap())
|
items = ItemList(totalCount, pageSize, cachedPages.asMap())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue