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:
parent
39c030c1d5
commit
a350fa2017
16 changed files with 211 additions and 76 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue