Improvements to "Add to playlist" dialog (#1706)

## Description
Fixes & improves the "Add to playlist" dialog:
- Added searching by name
- Playlists are sorted by recently updated
- No longer filters by media type
- Filters based on user's permission for the playlists

The behavior is now consistent with the web UI's list except for the
sort

### Related issues
Should fix #1124

### Testing
Emulator testing

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-16 11:35:48 -04:00 • committed by GitHub
parent 39c030c1d5
commit a350fa2017
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 211 additions and 76 deletions

View file

@ -15,13 +15,11 @@ import com.github.damontecres.wholphin.ui.gt
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.playback.playable
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.TransformList
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
import org.jellyfin.sdk.api.client.extensions.playlistsApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.videosApi
@ -344,26 +342,49 @@ class PlaylistCreator
* Get the playlists on the server for a given media type
*/
suspend fun getServerPlaylists(
query: String,
mediaType: MediaType?,
scope: CoroutineScope,
): List<PlaylistInfo?> {
): List<PlaylistInfo> {
val userId =
serverRepository.currentUser?.id ?: throw IllegalStateException("No user found")
val request =
GetItemsRequest(
searchTerm = query.takeIf { it.isNotBlank() },
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
mediaTypes = mediaType?.let { listOf(mediaType) },
recursive = true,
limit = 25,
sortBy = listOf(ItemSortBy.DATE_LAST_CONTENT_ADDED),
sortOrder = listOf(SortOrder.DESCENDING),
)
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,
)
val playlists = GetItemsRequestHandler.execute(api, request).content.items
return playlists
.mapNotNull { playlist ->
try {
val response = api.playlistsApi.getPlaylistUser(playlist.id, userId).content
if (response.canEdit) {
PlaylistInfo(
id = playlist.id,
name = playlist.name ?: context.getString(R.string.unknown),
count = playlist.childCount ?: 0,
mediaType = playlist.mediaType,
)
} else {
null
}
} catch (ex: InvalidStatusException) {
if (ex.status == 404) {
null
} else {
throw ex
}
}
}
}
// .sortedWith(
// compareBy<PlaylistInfo> {
// SearchRelevance.score(it.name, BaseItemKind.PLAYLIST, query)
// }.thenBy { it.name },
// )
}
suspend fun createServerPlaylist(

View file

@ -18,7 +18,6 @@ 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.nav.Destination
import org.jellyfin.sdk.model.api.MediaType
import java.util.UUID
/**
@ -76,7 +75,7 @@ class ContextMenuUtils(
provider.setFavorite(position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = provider::sendReportFor,
@ -129,6 +128,7 @@ class ContextMenuUtils(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -62,7 +62,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import timber.log.Timber
import java.util.UUID
@ -388,7 +387,7 @@ fun RecommendedContent(
viewModel.setFavorite(position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -485,12 +484,8 @@ fun RecommendedContent(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}
}
data class RowColumnItem(
val position: RowColumn,
val item: BaseItem,
)

View file

@ -18,7 +18,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -38,13 +37,13 @@ class AddPlaylistViewModel
) : ViewModel() {
val playlistState = MutableStateFlow<PlaylistLoadingState>(PlaylistLoadingState.Pending)
fun loadPlaylists(mediaType: MediaType?) {
fun loadPlaylists(query: String = "") {
viewModelScope.launchIO {
this@AddPlaylistViewModel.playlistState.value = PlaylistLoadingState.Loading
try {
val playlists = playlistCreator.getServerPlaylists(mediaType, viewModelScope)
val playlists = playlistCreator.getServerPlaylists(query, null)
this@AddPlaylistViewModel.playlistState.value =
PlaylistLoadingState.Success(playlists)
PlaylistLoadingState.Success(playlists, query)
} catch (ex: Exception) {
playlistState.value = PlaylistLoadingState.Error(ex)
}

View file

@ -412,7 +412,7 @@ fun PlaylistDetails(
onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) },
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
onClickAddPlaylist = { itemId ->
addToPlaylistViewModel.loadPlaylists(MediaType.AUDIO)
addToPlaylistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = { _, _ -> },
@ -425,7 +425,7 @@ fun PlaylistDetails(
onClickWatch = { id, watched -> viewModel.setWatched(id, watched) },
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
onClickAddPlaylist = { itemId ->
addToPlaylistViewModel.loadPlaylists(MediaType.VIDEO)
addToPlaylistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel::sendMediaReport,
@ -511,6 +511,7 @@ fun PlaylistDetails(
addToPlaylistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = addToPlaylistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -1,11 +1,16 @@
package com.github.damontecres.wholphin.ui.detail
import android.view.Gravity
import androidx.compose.foundation.focusable
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.fillMaxWidth
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@ -25,9 +30,13 @@ 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.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@ -38,6 +47,7 @@ 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.compose.ui.window.DialogWindowProvider
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme
@ -49,12 +59,16 @@ import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.SearchEditTextBox
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.tryRequestFocus
import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.api.MediaType
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun PlaylistList(
playlists: List<PlaylistInfo?>,
playlists: List<PlaylistInfo>,
onClick: (PlaylistInfo) -> Unit,
createEnabled: Boolean,
onCreatePlaylist: (String) -> Unit,
@ -68,33 +82,37 @@ fun PlaylistList(
items(playlists) { playlist ->
ListItem(
selected = false,
enabled = playlist != null,
enabled = true,
headlineContent = {
Text(
text = playlist?.name ?: stringResource(R.string.loading),
text = playlist.name,
)
},
supportingContent = {
if (playlist != null) {
Text(
text = playlist.mediaType.serialName,
)
}
val id =
when (playlist.mediaType) {
MediaType.UNKNOWN -> R.string.unknown
MediaType.VIDEO -> R.string.video
MediaType.AUDIO -> R.string.audio
MediaType.PHOTO -> R.string.photos
MediaType.BOOK -> R.string.unknown
}
Text(
text = stringResource(id),
)
},
trailingContent = {
if (playlist != null) {
Text(
text =
pluralStringResource(
R.plurals.items,
playlist.count,
playlist.count,
),
)
}
Text(
text =
pluralStringResource(
R.plurals.items,
playlist.count,
playlist.count,
),
)
},
onClick = {
if (playlist != null) onClick.invoke(playlist)
onClick.invoke(playlist)
},
modifier = Modifier,
)
@ -193,16 +211,38 @@ fun PlaylistDialog(
state: PlaylistLoadingState,
onDismissRequest: () -> Unit,
onClick: (PlaylistInfo) -> Unit,
onSearch: (String) -> Unit,
createEnabled: Boolean,
onCreatePlaylist: (String) -> Unit,
elevation: Dp = 3.dp,
) {
val elevatedContainerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
val placeholderFocusRequester = remember { FocusRequester() }
val focusRequester = remember { FocusRequester() }
val searchFocusRequester = remember { FocusRequester() }
var query by remember { mutableStateOf("") }
var searchClicked by remember { mutableStateOf(false) }
var searchHasFocus by remember { mutableStateOf(false) }
LaunchedEffect(query) {
val previousQuery = (state as? PlaylistLoadingState.Success)?.query ?: ""
if (previousQuery != query) {
delay(750.milliseconds)
if (searchClicked) {
searchClicked = false
return@LaunchedEffect
}
onSearch.invoke(query)
}
}
Dialog(
onDismissRequest = onDismissRequest,
) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
dialogWindowProvider?.window?.setGravity(Gravity.TOP)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
@ -220,6 +260,74 @@ fun PlaylistDialog(
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
SearchEditTextBox(
value = query,
onValueChange = { query = it },
onSearchClick = {
if (query.isNotBlank()) {
searchClicked = true
onSearch.invoke(query)
}
},
modifier =
Modifier
.fillMaxWidth()
.focusRequester(searchFocusRequester)
.onFocusChanged { searchHasFocus = it.hasFocus }
.focusProperties {
// Block focus to except for down/next to the results
// Don't want to focus on the placeholder
up = FocusRequester.Cancel
left = FocusRequester.Cancel
right = FocusRequester.Cancel
previous = FocusRequester.Cancel
start = FocusRequester.Cancel
end = FocusRequester.Cancel
down = focusRequester
next = focusRequester
},
)
LaunchedEffect(Unit) {
if (state !is PlaylistLoadingState.Success) {
placeholderFocusRequester.tryRequestFocus()
}
}
// Placeholder for initial focus
// After results load, focus will move to the first one
Box(
modifier =
Modifier
.align(Alignment.CenterHorizontally)
.width(0.dp)
.height(0.dp)
.focusRequester(placeholderFocusRequester)
.focusable(),
)
val resultsModifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
.focusProperties {
// Only focus up to search bar and ignore the placeholder
left = FocusRequester.Cancel
right = FocusRequester.Cancel
start = FocusRequester.Cancel
end = FocusRequester.Cancel
down = FocusRequester.Cancel
next = FocusRequester.Cancel
up = searchFocusRequester
previous = searchFocusRequester
onExit = {
if (requestedFocusDirection == FocusDirection.Next ||
requestedFocusDirection == FocusDirection.Left ||
requestedFocusDirection == FocusDirection.Right
) {
cancelFocusChange()
} else {
searchFocusRequester.tryRequestFocus()
}
}
}
when (val s = state) {
PlaylistLoadingState.Pending,
PlaylistLoadingState.Loading,
@ -234,20 +342,24 @@ fun PlaylistDialog(
}
is PlaylistLoadingState.Error -> {
ErrorMessage(s.message, s.exception)
ErrorMessage(
message = s.message,
exception = s.exception,
modifier = resultsModifier,
)
}
is PlaylistLoadingState.Success -> {
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
LaunchedEffect(Unit) {
// Once results load, focus unless the user is still focused on the search box
if (!searchHasFocus) focusRequester.tryRequestFocus()
}
PlaylistList(
playlists = s.items,
onClick = onClick,
createEnabled = createEnabled,
onCreatePlaylist = onCreatePlaylist,
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
modifier = resultsModifier,
)
}
}
@ -261,7 +373,8 @@ sealed interface PlaylistLoadingState {
data object Loading : PlaylistLoadingState
data class Success(
val items: List<PlaylistInfo?>,
val items: List<PlaylistInfo>,
val query: String,
) : PlaylistLoadingState
data class Error(

View file

@ -56,7 +56,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState
import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber
import java.util.UUID
@ -91,7 +90,7 @@ fun CollectionDetails(
viewModel.setFavorite(itemId, favorite, position)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -273,6 +272,7 @@ fun CollectionDetails(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -49,7 +49,6 @@ import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
@ -88,7 +87,7 @@ fun EpisodeDetails(
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -208,6 +207,7 @@ fun EpisodeDetails(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -70,7 +70,6 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
@ -109,7 +108,7 @@ fun MovieDetails(
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -275,6 +274,7 @@ fun MovieDetails(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -104,7 +104,6 @@ import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
import java.util.UUID
@ -337,7 +336,7 @@ fun AlbumDetailsPage(
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.AUDIO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = { _, _ -> },
@ -635,6 +634,7 @@ fun AlbumDetailsPage(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -99,7 +99,6 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
@ -328,7 +327,7 @@ fun ArtistDetailsPage(
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.AUDIO)
playlistViewModel.loadPlaylists()
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = { _, _ -> },
@ -631,6 +630,7 @@ fun ArtistDetailsPage(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -88,7 +88,6 @@ import com.github.damontecres.wholphin.util.DiscoverRequestType
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
import kotlin.time.Duration
@ -133,7 +132,7 @@ fun SeriesDetails(
viewModel.setFavorite(itemId, favorite, null)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog = Optional.present(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -307,6 +306,7 @@ fun SeriesDetails(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.PersonKind
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.UUIDSerializer
@ -116,7 +115,7 @@ fun SeriesOverview(
viewModel.setFavorite(itemId, favorite, currentPosition.episodeRowIndex)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog = itemId
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -348,6 +347,7 @@ fun SeriesOverview(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog = null
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -91,7 +91,6 @@ import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.DateTime
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration
@ -177,7 +176,7 @@ fun HomePage(
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
playlistViewModel.loadPlaylists()
showPlaylistDialog = itemId
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
@ -258,6 +257,7 @@ fun HomePage(
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog = null
},
onSearch = playlistViewModel::loadPlaylists,
elevation = 3.dp,
)
}

View file

@ -240,7 +240,7 @@ class SearchViewModel
val sorted =
items.sortedWith(
compareBy<BaseItem> { SearchRelevance.score(it, query) }
.thenBy { it.name ?: "" },
.thenBy { it.sortName },
)
_state.update { update.invoke(SearchResult.Success(sorted), it) }
} catch (ex: CancellationException) {

View file

@ -10,14 +10,20 @@ object SearchRelevance {
fun score(
item: BaseItem,
query: String,
): Int = score(item.name, item.type, query)
fun score(
name: String?,
type: BaseItemKind,
query: String,
): Int {
val name = item.name?.lowercase(Locale.getDefault()) ?: return Int.MAX_VALUE
val name = name?.lowercase(Locale.getDefault()) ?: return Int.MAX_VALUE
val q = query.lowercase(Locale.getDefault())
REGEX_PATTERN.find(query)?.let { match ->
val pattern = match.groupValues[1]
val caseInsensitive = match.groupValues[2] == "i"
return regexScore(item.name ?: "", pattern, caseInsensitive) + typeBonus(item.type)
return regexScore(name, pattern, caseInsensitive) + typeBonus(type)
}
return when {
@ -26,7 +32,7 @@ object SearchRelevance {
name.contains(" $q") -> 200
name.contains(q) -> 300
else -> fuzzyScore(name, q)
} + typeBonus(item.type)
} + typeBonus(type)
}
private fun regexScore(