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:
damontecres 2025-11-04 15:27:26 -05:00 committed by GitHub
parent 86977f846c
commit 40a8249469
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 540 additions and 0 deletions

View file

@ -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()),
)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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,

View file

@ -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(" - ")
}
}

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}
}
}

View file

@ -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])
}

View file

@ -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>