mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Support for playlists
This commit is contained in:
parent
07dabee8b9
commit
aab7e2eb17
11 changed files with 356 additions and 18 deletions
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.dolphin.data.model
|
||||||
|
|
||||||
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
||||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.util.seasonEpisode
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
|
@ -23,6 +24,13 @@ data class BaseItem(
|
||||||
|
|
||||||
@Transient val name = data.name
|
@Transient val name = data.name
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val title = if (type == BaseItemKind.EPISODE) data.seriesName else name
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val subtitle =
|
||||||
|
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
val indexNumber = data.indexNumber
|
val indexNumber = data.indexNumber
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ fun convertModel(
|
||||||
): DolphinModel =
|
): DolphinModel =
|
||||||
when (dto.type) {
|
when (dto.type) {
|
||||||
BaseItemKind.COLLECTION_FOLDER -> Library.fromDto(dto, api)
|
BaseItemKind.COLLECTION_FOLDER -> Library.fromDto(dto, api)
|
||||||
|
BaseItemKind.USER_VIEW -> Library.fromDto(dto, api)
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
BaseItemKind.VIDEO -> Video.fromDto(dto, api)
|
BaseItemKind.VIDEO -> Video.fromDto(dto, api)
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,15 @@ class PlaylistCreator
|
||||||
return Playlist(episodes.map { BaseItem.from(it, api) }, startIndex)
|
return Playlist(episodes.map { BaseItem.from(it, api) }, startIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createFromPlaylistId(playlistId: UUID): Playlist {
|
suspend fun createFromPlaylistId(
|
||||||
|
playlistId: UUID,
|
||||||
|
startIndex: Int?,
|
||||||
|
): Playlist {
|
||||||
val request =
|
val request =
|
||||||
GetPlaylistItemsRequest(
|
GetPlaylistItemsRequest(
|
||||||
playlistId = playlistId,
|
playlistId = playlistId,
|
||||||
fields = DefaultItemFields,
|
fields = DefaultItemFields,
|
||||||
|
startIndex = startIndex,
|
||||||
limit = Playlist.MAX_SIZE,
|
limit = Playlist.MAX_SIZE,
|
||||||
)
|
)
|
||||||
val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items
|
val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,6 @@ fun CollectionFolderGrid(
|
||||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection)
|
val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection)
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val library by viewModel.model.observeAsState()
|
|
||||||
val pager by viewModel.pager.observeAsState()
|
val pager by viewModel.pager.observeAsState()
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
|
|
@ -193,7 +192,6 @@ fun CollectionFolderGrid(
|
||||||
pager?.let { pager ->
|
pager?.let { pager ->
|
||||||
CollectionFolderGridContent(
|
CollectionFolderGridContent(
|
||||||
preferences,
|
preferences,
|
||||||
library!!,
|
|
||||||
item!!,
|
item!!,
|
||||||
pager,
|
pager,
|
||||||
sortAndDirection = sortAndDirection,
|
sortAndDirection = sortAndDirection,
|
||||||
|
|
@ -214,7 +212,6 @@ fun CollectionFolderGrid(
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderGridContent(
|
fun CollectionFolderGridContent(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
library: Library,
|
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
pager: List<BaseItem?>,
|
pager: List<BaseItem?>,
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
|
|
@ -225,7 +222,7 @@ fun CollectionFolderGridContent(
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val title = library.name ?: item.data.name ?: item.data.collectionType?.name ?: "Collection"
|
val title = item.name ?: item.data.collectionType?.name ?: "Collection"
|
||||||
val sortOptions =
|
val sortOptions =
|
||||||
when (item.data.collectionType) {
|
when (item.data.collectionType) {
|
||||||
CollectionType.MOVIES -> MovieSortOptions
|
CollectionType.MOVIES -> MovieSortOptions
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ fun OverviewText(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
textBoxHeight: Dp = maxLines * 20.dp,
|
textBoxHeight: Dp = maxLines * 20.dp,
|
||||||
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||||
|
|
@ -46,7 +47,7 @@ fun OverviewText(
|
||||||
.background(bgColor, shape = RoundedCornerShape(8.dp))
|
.background(bgColor, shape = RoundedCornerShape(8.dp))
|
||||||
.playSoundOnFocus(true)
|
.playSoundOnFocus(true)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = true,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = LocalIndication.current,
|
indication = LocalIndication.current,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ 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.BaseItem
|
||||||
import com.github.damontecres.dolphin.data.model.DolphinModel
|
|
||||||
import com.github.damontecres.dolphin.data.model.convertModel
|
|
||||||
import com.github.damontecres.dolphin.util.LoadingExceptionHandler
|
import com.github.damontecres.dolphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.dolphin.util.LoadingState
|
import com.github.damontecres.dolphin.util.LoadingState
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -22,16 +20,15 @@ import java.util.UUID
|
||||||
/**
|
/**
|
||||||
* Basic [ViewModel] for a single fetchable item from the API
|
* Basic [ViewModel] for a single fetchable item from the API
|
||||||
*/
|
*/
|
||||||
abstract class ItemViewModel<T : DolphinModel>(
|
abstract class ItemViewModel<T>(
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val item = MutableLiveData<BaseItem?>(null)
|
val item = MutableLiveData<BaseItem?>(null)
|
||||||
val model = MutableLiveData<T?>(null)
|
|
||||||
|
|
||||||
suspend fun fetchItem(
|
suspend fun fetchItem(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
potential: BaseItem?,
|
potential: BaseItem?,
|
||||||
): BaseItem? =
|
): BaseItem =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// val fetchedItem =
|
// val fetchedItem =
|
||||||
// when {
|
// when {
|
||||||
|
|
@ -44,11 +41,9 @@ abstract class ItemViewModel<T : DolphinModel>(
|
||||||
// }
|
// }
|
||||||
val it = api.userLibraryApi.getItem(itemId).content
|
val it = api.userLibraryApi.getItem(itemId).content
|
||||||
val fetchedItem = BaseItem.from(it, api)
|
val fetchedItem = BaseItem.from(it, api)
|
||||||
return@withContext fetchedItem?.let {
|
return@withContext fetchedItem.let {
|
||||||
val modelInstance = convertModel(fetchedItem.data, api)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
item.value = fetchedItem
|
item.value = fetchedItem
|
||||||
model.value = modelInstance as T
|
|
||||||
}
|
}
|
||||||
fetchedItem
|
fetchedItem
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +58,7 @@ abstract class ItemViewModel<T : DolphinModel>(
|
||||||
/**
|
/**
|
||||||
* Extends [ItemViewModel] to include a loading state tracking when the item has been fetched or if an error occurred
|
* Extends [ItemViewModel] to include a loading state tracking when the item has been fetched or if an error occurred
|
||||||
*/
|
*/
|
||||||
abstract class LoadingItemViewModel<T : DolphinModel>(
|
abstract class LoadingItemViewModel<T>(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
) : ItemViewModel<T>(api) {
|
) : ItemViewModel<T>(api) {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
|
|
@ -80,10 +75,8 @@ abstract class LoadingItemViewModel<T : DolphinModel>(
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedItem = api.userLibraryApi.getItem(itemId).content
|
val fetchedItem = api.userLibraryApi.getItem(itemId).content
|
||||||
val modelInstance = convertModel(fetchedItem, api)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
item.value = BaseItem.from(fetchedItem, api)
|
item.value = BaseItem.from(fetchedItem, api)
|
||||||
model.value = modelInstance as T
|
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.data.model.Library
|
||||||
|
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||||
|
import com.github.damontecres.dolphin.ui.cards.ItemCardImage
|
||||||
|
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.dolphin.ui.components.OverviewText
|
||||||
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.dolphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.dolphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.dolphin.util.GetPlaylistItemsRequestHandler
|
||||||
|
import com.github.damontecres.dolphin.util.LoadingExceptionHandler
|
||||||
|
import com.github.damontecres.dolphin.util.LoadingState
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class PlaylistViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
api: ApiClient,
|
||||||
|
val navigationManager: NavigationManager,
|
||||||
|
) : ItemViewModel<Library>(api) {
|
||||||
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
||||||
|
fun init(playlistId: UUID) {
|
||||||
|
loading.value = LoadingState.Loading
|
||||||
|
viewModelScope.launch(
|
||||||
|
Dispatchers.IO +
|
||||||
|
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
|
||||||
|
) {
|
||||||
|
val playlist = fetchItem(playlistId, null)
|
||||||
|
val request =
|
||||||
|
GetPlaylistItemsRequest(
|
||||||
|
playlistId = playlist.id,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
)
|
||||||
|
val pager = ApiRequestPager(api, request, GetPlaylistItemsRequestHandler, viewModelScope).init()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
items.value = pager
|
||||||
|
loading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistDetails(
|
||||||
|
destination: Destination.MediaItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: PlaylistViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(destination.itemId)
|
||||||
|
}
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||||
|
val playlist by viewModel.item.observeAsState(null)
|
||||||
|
val items by viewModel.items.observeAsState(listOf())
|
||||||
|
|
||||||
|
when (val st = loading) {
|
||||||
|
is LoadingState.Error -> ErrorMessage(st, modifier)
|
||||||
|
LoadingState.Pending, LoadingState.Loading -> LoadingPage(modifier)
|
||||||
|
LoadingState.Success ->
|
||||||
|
playlist?.let {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
PlaylistDetailsContent(
|
||||||
|
playlist = it,
|
||||||
|
items = items,
|
||||||
|
onClickIndex = { index ->
|
||||||
|
viewModel.navigationManager.navigateTo(
|
||||||
|
Destination.Playback(
|
||||||
|
itemId = it.id,
|
||||||
|
positionMs = 0L,
|
||||||
|
startIndex = index,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistDetailsContent(
|
||||||
|
playlist: BaseItem,
|
||||||
|
items: List<BaseItem?>,
|
||||||
|
onClickIndex: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var savedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
val focusedItem = items.getOrNull(focusedIndex)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) {
|
||||||
|
val gradientColor = MaterialTheme.colorScheme.background
|
||||||
|
AsyncImage(
|
||||||
|
model = focusedItem.backdropImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
alignment = Alignment.TopEnd,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(.85f)
|
||||||
|
.alpha(.4f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(Color.Transparent, gradientColor),
|
||||||
|
startY = 500f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(gradientColor, Color.Transparent),
|
||||||
|
endX = 400f,
|
||||||
|
startX = 100f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp, top = 16.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = playlist.name ?: "Playlist",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
)
|
||||||
|
PlaylistDetailsHeader(
|
||||||
|
focusedItem = focusedItem,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxWidth(.66f),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.8f)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
).focusGroup()
|
||||||
|
.focusRestorer(focusRequester),
|
||||||
|
) {
|
||||||
|
itemsIndexed(items) { index, item ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
ListItem(
|
||||||
|
selected = false,
|
||||||
|
onClick = {
|
||||||
|
savedIndex = index
|
||||||
|
onClickIndex.invoke(index)
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = item?.title ?: "",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = item?.subtitle ?: "",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
item?.data?.runTimeTicks?.ticks?.roundMinutes?.let {
|
||||||
|
Text(
|
||||||
|
text = it.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${index + 1}.",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
ItemCardImage(
|
||||||
|
imageUrl = item?.imageUrl,
|
||||||
|
name = item?.name,
|
||||||
|
showOverlay = true,
|
||||||
|
favorite = item?.data?.userData?.isFavorite ?: false,
|
||||||
|
watched = item?.data?.userData?.played ?: false,
|
||||||
|
unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1,
|
||||||
|
watchedPercent = 0.0,
|
||||||
|
modifier = Modifier.width(160.dp),
|
||||||
|
useFallbackText = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.height(80.dp)
|
||||||
|
.ifElse(
|
||||||
|
index == savedIndex,
|
||||||
|
Modifier.focusRequester(focusRequester),
|
||||||
|
).onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
focusedIndex = index
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistDetailsHeader(
|
||||||
|
focusedItem: BaseItem?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = focusedItem?.title ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = focusedItem?.subtitle ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
OverviewText(
|
||||||
|
overview = focusedItem?.data?.overview ?: "",
|
||||||
|
maxLines = 2,
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,6 +67,7 @@ sealed class Destination(
|
||||||
val itemId: UUID,
|
val itemId: UUID,
|
||||||
val positionMs: Long,
|
val positionMs: Long,
|
||||||
@Transient val item: BaseItem? = null,
|
@Transient val item: BaseItem? = null,
|
||||||
|
val startIndex: Int? = null,
|
||||||
) : Destination(true) {
|
) : Destination(true) {
|
||||||
override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)"
|
override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.github.damontecres.dolphin.ui.detail.CollectionFolderGeneric
|
||||||
import com.github.damontecres.dolphin.ui.detail.CollectionFolderMovie
|
import com.github.damontecres.dolphin.ui.detail.CollectionFolderMovie
|
||||||
import com.github.damontecres.dolphin.ui.detail.CollectionFolderTv
|
import com.github.damontecres.dolphin.ui.detail.CollectionFolderTv
|
||||||
import com.github.damontecres.dolphin.ui.detail.EpisodeDetails
|
import com.github.damontecres.dolphin.ui.detail.EpisodeDetails
|
||||||
|
import com.github.damontecres.dolphin.ui.detail.PlaylistDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.SeriesDetails
|
import com.github.damontecres.dolphin.ui.detail.SeriesDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails
|
import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails
|
||||||
|
|
@ -130,6 +131,19 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BaseItemKind.PLAYLIST ->
|
||||||
|
PlaylistDetails(
|
||||||
|
destination = destination,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
BaseItemKind.USER_VIEW ->
|
||||||
|
CollectionFolderGeneric(
|
||||||
|
preferences,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Text("Unsupported item type: ${destination.type}")
|
Text("Unsupported item type: ${destination.type}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,11 @@ class PlaybackViewModel
|
||||||
val base =
|
val base =
|
||||||
if (queriedItem.type == BaseItemKind.PLAYLIST) {
|
if (queriedItem.type == BaseItemKind.PLAYLIST) {
|
||||||
isPlaylist = true
|
isPlaylist = true
|
||||||
val playlist = playlistCreator.createFromPlaylistId(queriedItem.id)
|
val playlist =
|
||||||
|
playlistCreator.createFromPlaylistId(
|
||||||
|
queriedItem.id,
|
||||||
|
destination.startIndex,
|
||||||
|
)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@PlaybackViewModel.playlist.value = playlist
|
this@PlaybackViewModel.playlist.value = playlist
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,5 @@ val supportedCollectionTypes =
|
||||||
CollectionType.MOVIES,
|
CollectionType.MOVIES,
|
||||||
CollectionType.TVSHOWS,
|
CollectionType.TVSHOWS,
|
||||||
CollectionType.HOMEVIDEOS,
|
CollectionType.HOMEVIDEOS,
|
||||||
|
CollectionType.PLAYLISTS,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue