From a350fa20176c4ff024d086fab86301293f16e5f7 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Thu, 16 Jul 2026 11:35:48 -0400 Subject: [PATCH] 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 --- .../wholphin/services/PlaylistCreator.kt | 51 ++++-- .../ui/components/ContextMenuUtils.kt | 4 +- .../ui/components/RecommendedContent.kt | 9 +- .../wholphin/ui/data/AddPlaylistViewModel.kt | 7 +- .../wholphin/ui/detail/PlaylistDetails.kt | 5 +- .../wholphin/ui/detail/PlaylistList.kt | 165 +++++++++++++++--- .../ui/detail/collection/CollectionDetails.kt | 4 +- .../ui/detail/episode/EpisodeDetails.kt | 4 +- .../wholphin/ui/detail/movie/MovieDetails.kt | 4 +- .../ui/detail/music/AlbumDetailsPage.kt | 4 +- .../ui/detail/music/ArtistDetailsPage.kt | 4 +- .../ui/detail/series/SeriesDetails.kt | 4 +- .../ui/detail/series/SeriesOverview.kt | 4 +- .../damontecres/wholphin/ui/main/HomePage.kt | 4 +- .../wholphin/ui/main/SearchPage.kt | 2 +- .../wholphin/util/SearchRelevance.kt | 12 +- 16 files changed, 211 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index 0b2e85e3..22ecb1ca 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -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 { + ): List { + 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 { +// SearchRelevance.score(it.name, BaseItemKind.PLAYLIST, query) +// }.thenBy { it.name }, +// ) } suspend fun createServerPlaylist( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt index 80e989ea..2261e0ac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index 7dc5d11a..7159fc58 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -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, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt index a41487c7..4dc1dd16 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt @@ -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.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) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 7d66c08b..5859df56 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt index df4408fb..e38ff099 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt @@ -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, + playlists: List, 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, + val items: List, + val query: String, ) : PlaylistLoadingState data class Error( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt index 3077727c..f22c7f33 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index dd91e00b..ff43eb10 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 42742c85..bc4a3461 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt index 55dc6b4c..f7f50bdb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt index d8806b59..eacc0e5a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 69639590..fe0b5e5c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index fed2d342..77922f9d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 27d84670..cc06ed88 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index ea978971..b1705970 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -240,7 +240,7 @@ class SearchViewModel val sorted = items.sortedWith( compareBy { SearchRelevance.score(it, query) } - .thenBy { it.name ?: "" }, + .thenBy { it.sortName }, ) _state.update { update.invoke(SearchResult.Success(sorted), it) } } catch (ex: CancellationException) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/SearchRelevance.kt b/app/src/main/java/com/github/damontecres/wholphin/util/SearchRelevance.kt index 60dada9d..dd0d0b38 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/SearchRelevance.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/SearchRelevance.kt @@ -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(