mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Support basic adding to playlists (#157)
Closes #143 Adds option in the "More" menu to add the item to a playlist. Can also create a playlist if needed.
This commit is contained in:
parent
86977f846c
commit
40a8249469
9 changed files with 540 additions and 0 deletions
|
|
@ -1,15 +1,31 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetPlaylistItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.TransformList
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.playlistsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CreatePlaylistDto
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlaylistUserPermissions
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -49,11 +65,20 @@ class Playlist(
|
|||
}
|
||||
}
|
||||
|
||||
data class PlaylistInfo(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val count: Int,
|
||||
val mediaType: MediaType,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
class PlaylistCreator
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
) {
|
||||
suspend fun createFromEpisode(
|
||||
seriesId: UUID,
|
||||
|
|
@ -92,4 +117,61 @@ class PlaylistCreator
|
|||
}
|
||||
return Playlist(baseItems, 0)
|
||||
}
|
||||
|
||||
suspend fun getPlaylists(
|
||||
mediaType: MediaType?,
|
||||
scope: CoroutineScope,
|
||||
): List<PlaylistInfo?> {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
|
||||
mediaTypes = mediaType?.let { listOf(mediaType) },
|
||||
recursive = true,
|
||||
)
|
||||
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, scope).init()
|
||||
return TransformList(pager) {
|
||||
it?.let {
|
||||
PlaylistInfo(
|
||||
id = it.id,
|
||||
name = it.name ?: context.getString(R.string.unknown),
|
||||
count = it.data.childCount ?: 0,
|
||||
mediaType = it.data.mediaType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createPlaylist(
|
||||
name: String,
|
||||
initialItems: List<UUID>,
|
||||
): UUID? =
|
||||
serverRepository.currentUser?.let { user ->
|
||||
api.playlistsApi
|
||||
.createPlaylist(
|
||||
CreatePlaylistDto(
|
||||
name = name,
|
||||
ids = initialItems,
|
||||
users = listOf(PlaylistUserPermissions(user.id, true)),
|
||||
isPublic = false,
|
||||
),
|
||||
).content.id
|
||||
.toUUIDOrNull()
|
||||
}
|
||||
|
||||
suspend fun addToPlaylist(
|
||||
playlistId: UUID,
|
||||
itemId: UUID,
|
||||
) {
|
||||
api.playlistsApi.addItemToPlaylist(playlistId, listOf(itemId))
|
||||
}
|
||||
|
||||
suspend fun removeFromPlaylist(
|
||||
playlistId: UUID,
|
||||
itemId: UUID,
|
||||
) {
|
||||
api.playlistsApi.removeItemFromPlaylist(
|
||||
playlistId.toServerString(),
|
||||
listOf(itemId.toServerString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package com.github.damontecres.wholphin.ui.data
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.model.PlaylistCreator
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AddPlaylistViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val playlistCreator: PlaylistCreator,
|
||||
) : ViewModel() {
|
||||
val playlistState = MutableLiveData<PlaylistLoadingState>(PlaylistLoadingState.Pending)
|
||||
|
||||
fun loadPlaylists(mediaType: MediaType?) {
|
||||
viewModelScope.launchIO {
|
||||
this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Loading)
|
||||
try {
|
||||
val playlists = playlistCreator.getPlaylists(mediaType, viewModelScope)
|
||||
this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Success(playlists))
|
||||
} catch (ex: Exception) {
|
||||
playlistState.setValueOnMain(PlaylistLoadingState.Error(ex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addToPlaylist(
|
||||
playlistId: UUID,
|
||||
itemId: UUID,
|
||||
) {
|
||||
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
||||
playlistCreator.addToPlaylist(playlistId, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
fun createPlaylistAndAddItem(
|
||||
playlistName: String,
|
||||
itemId: UUID,
|
||||
) {
|
||||
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
||||
val playlistId = playlistCreator.createPlaylist(playlistName, listOf(itemId))
|
||||
if (playlistId == null) {
|
||||
showToast(context, "Error creating playlist", Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ fun buildMoreDialogItems(
|
|||
onClickFavorite: (Boolean) -> Unit,
|
||||
onChooseVersion: () -> Unit,
|
||||
onChooseTracks: (MediaStreamType) -> Unit,
|
||||
onClickAddPlaylist: () -> Unit,
|
||||
): List<DialogItem> =
|
||||
buildList {
|
||||
add(
|
||||
|
|
@ -113,6 +114,14 @@ fun buildMoreDialogItems(
|
|||
)
|
||||
}
|
||||
}
|
||||
add(
|
||||
DialogItem(
|
||||
text = R.string.add_to_playlist,
|
||||
iconStringRes = R.string.fa_list_ul,
|
||||
) {
|
||||
onClickAddPlaylist.invoke()
|
||||
},
|
||||
)
|
||||
add(
|
||||
DialogItem(
|
||||
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||
|
|
@ -173,6 +182,7 @@ fun buildMoreDialogItemsForHome(
|
|||
navigateTo: (Destination) -> Unit,
|
||||
onClickWatch: (UUID, Boolean) -> Unit,
|
||||
onClickFavorite: (UUID, Boolean) -> Unit,
|
||||
onClickAddPlaylist: (UUID) -> Unit,
|
||||
): List<DialogItem> =
|
||||
buildList {
|
||||
val itemId = item.id
|
||||
|
|
@ -231,6 +241,14 @@ fun buildMoreDialogItemsForHome(
|
|||
)
|
||||
}
|
||||
}
|
||||
add(
|
||||
DialogItem(
|
||||
text = R.string.add_to_playlist,
|
||||
iconStringRes = R.string.fa_list_ul,
|
||||
) {
|
||||
onClickAddPlaylist.invoke(itemId)
|
||||
},
|
||||
)
|
||||
add(
|
||||
DialogItem(
|
||||
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
package com.github.damontecres.wholphin.ui.detail
|
||||
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.PlaylistInfo
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
||||
@Composable
|
||||
fun PlaylistList(
|
||||
playlists: List<PlaylistInfo?>,
|
||||
onClick: (PlaylistInfo) -> Unit,
|
||||
createEnabled: Boolean,
|
||||
onCreatePlaylist: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(playlists) { playlist ->
|
||||
ListItem(
|
||||
selected = false,
|
||||
enabled = playlist != null,
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = playlist?.name ?: stringResource(R.string.loading),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
if (playlist != null) {
|
||||
Text(
|
||||
text = playlist.mediaType.serialName,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
if (playlist != null) {
|
||||
Text(
|
||||
text =
|
||||
pluralStringResource(
|
||||
R.plurals.items,
|
||||
playlist.count,
|
||||
playlist.count,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (playlist != null) onClick.invoke(playlist)
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (createEnabled) {
|
||||
item {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
selected = false,
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.create_playlist),
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
tint = Color.Green.copy(.7f),
|
||||
contentDescription = "Add",
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showCreateDialog = true
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCreateDialog) {
|
||||
BasicDialog(
|
||||
onDismissRequest = {
|
||||
showCreateDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
elevation = 10.dp,
|
||||
) {
|
||||
var playlistName by remember { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(.4f),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.name),
|
||||
)
|
||||
EditTextBox(
|
||||
value = playlistName,
|
||||
onValueChange = {
|
||||
playlistName = it
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Words,
|
||||
autoCorrectEnabled = true,
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onDone = {
|
||||
showCreateDialog = false
|
||||
onCreatePlaylist.invoke(playlistName)
|
||||
},
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
showCreateDialog = false
|
||||
onCreatePlaylist.invoke(playlistName)
|
||||
},
|
||||
enabled = playlistName.isNotNullOrBlank(),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
Text(text = stringResource(R.string.submit))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaylistDialog(
|
||||
title: String,
|
||||
state: PlaylistLoadingState,
|
||||
onDismissRequest: () -> Unit,
|
||||
onClick: (PlaylistInfo) -> Unit,
|
||||
createEnabled: Boolean,
|
||||
onCreatePlaylist: (String) -> Unit,
|
||||
elevation: Dp = 3.dp,
|
||||
) {
|
||||
val elevatedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(24.0.dp)
|
||||
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||
.padding(PaddingValues(16.dp)),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
when (val s = state) {
|
||||
PlaylistLoadingState.Pending,
|
||||
PlaylistLoadingState.Loading,
|
||||
->
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.border,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.size(48.dp),
|
||||
)
|
||||
|
||||
is PlaylistLoadingState.Error ->
|
||||
ErrorMessage(s.message, s.exception)
|
||||
|
||||
is PlaylistLoadingState.Success -> {
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
PlaylistList(
|
||||
playlists = s.items,
|
||||
onClick = onClick,
|
||||
createEnabled = createEnabled,
|
||||
onCreatePlaylist = onCreatePlaylist,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface PlaylistLoadingState {
|
||||
data object Pending : PlaylistLoadingState
|
||||
|
||||
data object Loading : PlaylistLoadingState
|
||||
|
||||
data class Success(
|
||||
val items: List<PlaylistInfo?>,
|
||||
) : PlaylistLoadingState
|
||||
|
||||
data class Error(
|
||||
val message: String? = null,
|
||||
val exception: Throwable? = null,
|
||||
) : PlaylistLoadingState {
|
||||
constructor(exception: Throwable) : this(null, exception)
|
||||
|
||||
val localizedMessage: String =
|
||||
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
|
||||
}
|
||||
}
|
||||
|
|
@ -65,8 +65,11 @@ import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
|||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.chooseStream
|
||||
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -76,6 +79,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
|||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -86,6 +90,7 @@ fun MovieDetails(
|
|||
destination: Destination.MediaItem,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: MovieViewModel = hiltViewModel(),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -102,6 +107,8 @@ fun MovieDetails(
|
|||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf(false) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
|
@ -206,6 +213,10 @@ fun MovieDetails(
|
|||
)
|
||||
}
|
||||
},
|
||||
onClickAddPlaylist = {
|
||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||
showPlaylistDialog = true
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -267,6 +278,23 @@ fun MovieDetails(
|
|||
waitToLoad = params.fromLongClick,
|
||||
)
|
||||
}
|
||||
if (showPlaylistDialog) {
|
||||
PlaylistDialog(
|
||||
title = stringResource(R.string.add_to_playlist),
|
||||
state = playlistState,
|
||||
onDismissRequest = { showPlaylistDialog = false },
|
||||
onClick = {
|
||||
playlistViewModel.addToPlaylist(it.id, destination.itemId)
|
||||
showPlaylistDialog = false
|
||||
},
|
||||
createEnabled = true,
|
||||
onCreatePlaylist = {
|
||||
playlistViewModel.createPlaylistAndAddItem(it, destination.itemId)
|
||||
showPlaylistDialog = false
|
||||
},
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_ROW = 0
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import com.github.damontecres.wholphin.R
|
||||
|
|
@ -27,8 +29,11 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
|||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.chooseStream
|
||||
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
|
|
@ -39,6 +44,7 @@ import com.github.damontecres.wholphin.util.seasonEpisode
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
|
|
@ -72,6 +78,7 @@ fun SeriesOverview(
|
|||
destination: Destination.SeriesOverview,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SeriesViewModel = hiltViewModel(),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
initialSeasonEpisode: SeasonEpisodeIds? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -127,6 +134,8 @@ fun SeriesOverview(
|
|||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
LaunchedEffect(episodes) {
|
||||
episodes?.let { episodes ->
|
||||
|
|
@ -245,6 +254,10 @@ fun SeriesOverview(
|
|||
)
|
||||
}
|
||||
},
|
||||
onClickAddPlaylist = {
|
||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||
showPlaylistDialog = ep.id
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -363,4 +376,21 @@ fun SeriesOverview(
|
|||
waitToLoad = params.fromLongClick,
|
||||
)
|
||||
}
|
||||
showPlaylistDialog?.let { itemId ->
|
||||
PlaylistDialog(
|
||||
title = stringResource(R.string.add_to_playlist),
|
||||
state = playlistState,
|
||||
onDismissRequest = { showPlaylistDialog = null },
|
||||
onClick = {
|
||||
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||
showPlaylistDialog = null
|
||||
},
|
||||
createEnabled = true,
|
||||
onCreatePlaylist = {
|
||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||
showPlaylistDialog = null
|
||||
},
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
|
|
@ -55,8 +56,11 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup
|
|||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
|
|
@ -68,7 +72,9 @@ import com.github.damontecres.wholphin.util.formatDateTime
|
|||
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class HomeRow(
|
||||
|
|
@ -92,6 +98,7 @@ fun HomePage(
|
|||
preferences: UserPreferences,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: HomeViewModel = hiltViewModel(),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var firstLoad by rememberSaveable { mutableStateOf(true) }
|
||||
|
|
@ -124,6 +131,8 @@ fun HomePage(
|
|||
|
||||
LoadingState.Success -> {
|
||||
var dialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
HomePageContent(
|
||||
homeRows,
|
||||
onClickItem = {
|
||||
|
|
@ -149,6 +158,10 @@ fun HomePage(
|
|||
onClickFavorite = { itemId, favorite ->
|
||||
viewModel.setFavorite(itemId, favorite)
|
||||
},
|
||||
onClickAddPlaylist = { itemId ->
|
||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||
showPlaylistDialog = itemId
|
||||
},
|
||||
)
|
||||
dialog =
|
||||
DialogParams(
|
||||
|
|
@ -166,6 +179,23 @@ fun HomePage(
|
|||
onDismissRequest = { dialog = null },
|
||||
)
|
||||
}
|
||||
showPlaylistDialog?.let { itemId ->
|
||||
PlaylistDialog(
|
||||
title = stringResource(R.string.add_to_playlist),
|
||||
state = playlistState,
|
||||
onDismissRequest = { showPlaylistDialog = null },
|
||||
onClick = {
|
||||
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||
showPlaylistDialog = null
|
||||
},
|
||||
createEnabled = true,
|
||||
onCreatePlaylist = {
|
||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||
showPlaylistDialog = null
|
||||
},
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.github.damontecres.wholphin.util
|
||||
|
||||
class TransformList<S, T>(
|
||||
private val source: List<S>,
|
||||
private val transform: (S) -> T,
|
||||
) : AbstractList<T>() {
|
||||
override val size: Int
|
||||
get() = source.size
|
||||
|
||||
override fun get(index: Int): T = transform.invoke(source[index])
|
||||
}
|
||||
|
|
@ -139,6 +139,8 @@
|
|||
<string name="years_old">%1$d years old</string>
|
||||
<string name="play_with_transcoding">Play with transcoding</string>
|
||||
<string name="aired_episode_order">Aired Episode Order</string>
|
||||
<string name="create_playlist">Create new playlist</string>
|
||||
<string name="add_to_playlist">Add to playlist</string>
|
||||
|
||||
<plurals name="downloads">
|
||||
<item quantity="zero">%d downloads</item>
|
||||
|
|
@ -150,6 +152,11 @@
|
|||
<item quantity="one">%d hour</item>
|
||||
<item quantity="other">%d hours</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="zero">%d items</item>
|
||||
<item quantity="one">%d item</item>
|
||||
<item quantity="other">%d items</item>
|
||||
</plurals>
|
||||
<plurals name="seconds">
|
||||
<item quantity="zero">%d seconds</item>
|
||||
<item quantity="one">%d second</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue