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.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.playback.playable
|
import com.github.damontecres.wholphin.ui.playback.playable
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
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.GetEpisodesRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.TransformList
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
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.playlistsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
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
|
* Get the playlists on the server for a given media type
|
||||||
*/
|
*/
|
||||||
suspend fun getServerPlaylists(
|
suspend fun getServerPlaylists(
|
||||||
|
query: String,
|
||||||
mediaType: MediaType?,
|
mediaType: MediaType?,
|
||||||
scope: CoroutineScope,
|
): List<PlaylistInfo> {
|
||||||
): List<PlaylistInfo?> {
|
val userId =
|
||||||
|
serverRepository.currentUser?.id ?: throw IllegalStateException("No user found")
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
|
searchTerm = query.takeIf { it.isNotBlank() },
|
||||||
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
|
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
|
||||||
mediaTypes = mediaType?.let { listOf(mediaType) },
|
mediaTypes = mediaType?.let { listOf(mediaType) },
|
||||||
recursive = true,
|
recursive = true,
|
||||||
|
limit = 25,
|
||||||
|
sortBy = listOf(ItemSortBy.DATE_LAST_CONTENT_ADDED),
|
||||||
|
sortOrder = listOf(SortOrder.DESCENDING),
|
||||||
)
|
)
|
||||||
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, scope).init()
|
val playlists = GetItemsRequestHandler.execute(api, request).content.items
|
||||||
return TransformList(pager) {
|
return playlists
|
||||||
it?.let {
|
.mapNotNull { playlist ->
|
||||||
PlaylistInfo(
|
try {
|
||||||
id = it.id,
|
val response = api.playlistsApi.getPlaylistUser(playlist.id, userId).content
|
||||||
name = it.name ?: context.getString(R.string.unknown),
|
if (response.canEdit) {
|
||||||
count = it.data.childCount ?: 0,
|
PlaylistInfo(
|
||||||
mediaType = it.data.mediaType,
|
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(
|
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.data.ItemDetailsDialogInfo
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,7 +75,7 @@ class ContextMenuUtils(
|
||||||
provider.setFavorite(position, itemId, favorite)
|
provider.setFavorite(position, itemId, favorite)
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = provider::sendReportFor,
|
onSendMediaInfo = provider::sendReportFor,
|
||||||
|
|
@ -129,6 +128,7 @@ class ContextMenuUtils(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -388,7 +387,7 @@ fun RecommendedContent(
|
||||||
viewModel.setFavorite(position, itemId, favorite)
|
viewModel.setFavorite(position, itemId, favorite)
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -485,12 +484,8 @@ fun RecommendedContent(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -38,13 +37,13 @@ class AddPlaylistViewModel
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val playlistState = MutableStateFlow<PlaylistLoadingState>(PlaylistLoadingState.Pending)
|
val playlistState = MutableStateFlow<PlaylistLoadingState>(PlaylistLoadingState.Pending)
|
||||||
|
|
||||||
fun loadPlaylists(mediaType: MediaType?) {
|
fun loadPlaylists(query: String = "") {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
this@AddPlaylistViewModel.playlistState.value = PlaylistLoadingState.Loading
|
this@AddPlaylistViewModel.playlistState.value = PlaylistLoadingState.Loading
|
||||||
try {
|
try {
|
||||||
val playlists = playlistCreator.getServerPlaylists(mediaType, viewModelScope)
|
val playlists = playlistCreator.getServerPlaylists(query, null)
|
||||||
this@AddPlaylistViewModel.playlistState.value =
|
this@AddPlaylistViewModel.playlistState.value =
|
||||||
PlaylistLoadingState.Success(playlists)
|
PlaylistLoadingState.Success(playlists, query)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
playlistState.value = PlaylistLoadingState.Error(ex)
|
playlistState.value = PlaylistLoadingState.Error(ex)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ fun PlaylistDetails(
|
||||||
onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) },
|
onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) },
|
||||||
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
|
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
addToPlaylistViewModel.loadPlaylists(MediaType.AUDIO)
|
addToPlaylistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onClickRemoveFromQueue = { _, _ -> },
|
onClickRemoveFromQueue = { _, _ -> },
|
||||||
|
|
@ -425,7 +425,7 @@ fun PlaylistDetails(
|
||||||
onClickWatch = { id, watched -> viewModel.setWatched(id, watched) },
|
onClickWatch = { id, watched -> viewModel.setWatched(id, watched) },
|
||||||
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
|
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
addToPlaylistViewModel.loadPlaylists(MediaType.VIDEO)
|
addToPlaylistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel::sendMediaReport,
|
onSendMediaInfo = viewModel::sendMediaReport,
|
||||||
|
|
@ -511,6 +511,7 @@ fun PlaylistDetails(
|
||||||
addToPlaylistViewModel.createPlaylistAndAddItem(it, itemId)
|
addToPlaylistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = addToPlaylistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -25,9 +30,13 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
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.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.ListItem
|
import androidx.tv.material3.ListItem
|
||||||
import androidx.tv.material3.MaterialTheme
|
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.Button
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
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.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
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
|
@Composable
|
||||||
fun PlaylistList(
|
fun PlaylistList(
|
||||||
playlists: List<PlaylistInfo?>,
|
playlists: List<PlaylistInfo>,
|
||||||
onClick: (PlaylistInfo) -> Unit,
|
onClick: (PlaylistInfo) -> Unit,
|
||||||
createEnabled: Boolean,
|
createEnabled: Boolean,
|
||||||
onCreatePlaylist: (String) -> Unit,
|
onCreatePlaylist: (String) -> Unit,
|
||||||
|
|
@ -68,33 +82,37 @@ fun PlaylistList(
|
||||||
items(playlists) { playlist ->
|
items(playlists) { playlist ->
|
||||||
ListItem(
|
ListItem(
|
||||||
selected = false,
|
selected = false,
|
||||||
enabled = playlist != null,
|
enabled = true,
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
text = playlist?.name ?: stringResource(R.string.loading),
|
text = playlist.name,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
if (playlist != null) {
|
val id =
|
||||||
Text(
|
when (playlist.mediaType) {
|
||||||
text = playlist.mediaType.serialName,
|
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 = {
|
trailingContent = {
|
||||||
if (playlist != null) {
|
Text(
|
||||||
Text(
|
text =
|
||||||
text =
|
pluralStringResource(
|
||||||
pluralStringResource(
|
R.plurals.items,
|
||||||
R.plurals.items,
|
playlist.count,
|
||||||
playlist.count,
|
playlist.count,
|
||||||
playlist.count,
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (playlist != null) onClick.invoke(playlist)
|
onClick.invoke(playlist)
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
|
|
@ -193,16 +211,38 @@ fun PlaylistDialog(
|
||||||
state: PlaylistLoadingState,
|
state: PlaylistLoadingState,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onClick: (PlaylistInfo) -> Unit,
|
onClick: (PlaylistInfo) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
createEnabled: Boolean,
|
createEnabled: Boolean,
|
||||||
onCreatePlaylist: (String) -> Unit,
|
onCreatePlaylist: (String) -> Unit,
|
||||||
elevation: Dp = 3.dp,
|
elevation: Dp = 3.dp,
|
||||||
) {
|
) {
|
||||||
val elevatedContainerColor =
|
val elevatedContainerColor =
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||||
|
|
||||||
|
val placeholderFocusRequester = remember { FocusRequester() }
|
||||||
val focusRequester = 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(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
|
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||||
|
dialogWindowProvider?.window?.setGravity(Gravity.TOP)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -220,6 +260,74 @@ fun PlaylistDialog(
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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) {
|
when (val s = state) {
|
||||||
PlaylistLoadingState.Pending,
|
PlaylistLoadingState.Pending,
|
||||||
PlaylistLoadingState.Loading,
|
PlaylistLoadingState.Loading,
|
||||||
|
|
@ -234,20 +342,24 @@ fun PlaylistDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
is PlaylistLoadingState.Error -> {
|
is PlaylistLoadingState.Error -> {
|
||||||
ErrorMessage(s.message, s.exception)
|
ErrorMessage(
|
||||||
|
message = s.message,
|
||||||
|
exception = s.exception,
|
||||||
|
modifier = resultsModifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is PlaylistLoadingState.Success -> {
|
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(
|
PlaylistList(
|
||||||
playlists = s.items,
|
playlists = s.items,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
createEnabled = createEnabled,
|
createEnabled = createEnabled,
|
||||||
onCreatePlaylist = onCreatePlaylist,
|
onCreatePlaylist = onCreatePlaylist,
|
||||||
modifier =
|
modifier = resultsModifier,
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +373,8 @@ sealed interface PlaylistLoadingState {
|
||||||
data object Loading : PlaylistLoadingState
|
data object Loading : PlaylistLoadingState
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val items: List<PlaylistInfo?>,
|
val items: List<PlaylistInfo>,
|
||||||
|
val query: String,
|
||||||
) : PlaylistLoadingState
|
) : PlaylistLoadingState
|
||||||
|
|
||||||
data class Error(
|
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.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -91,7 +90,7 @@ fun CollectionDetails(
|
||||||
viewModel.setFavorite(itemId, favorite, position)
|
viewModel.setFavorite(itemId, favorite, position)
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -273,6 +272,7 @@ fun CollectionDetails(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
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.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -88,7 +87,7 @@ fun EpisodeDetails(
|
||||||
onClickWatch = viewModel::setWatched,
|
onClickWatch = viewModel::setWatched,
|
||||||
onClickFavorite = viewModel::setFavorite,
|
onClickFavorite = viewModel::setFavorite,
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -208,6 +207,7 @@ fun EpisodeDetails(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
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.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -109,7 +108,7 @@ fun MovieDetails(
|
||||||
onClickWatch = viewModel::setWatched,
|
onClickWatch = viewModel::setWatched,
|
||||||
onClickFavorite = viewModel::setFavorite,
|
onClickFavorite = viewModel::setFavorite,
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -275,6 +274,7 @@ fun MovieDetails(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
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.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
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.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -337,7 +336,7 @@ fun AlbumDetailsPage(
|
||||||
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
|
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
|
||||||
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.AUDIO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onClickRemoveFromQueue = { _, _ -> },
|
onClickRemoveFromQueue = { _, _ -> },
|
||||||
|
|
@ -635,6 +634,7 @@ fun AlbumDetailsPage(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
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.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
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.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||||
|
|
@ -328,7 +327,7 @@ fun ArtistDetailsPage(
|
||||||
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
|
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
|
||||||
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.AUDIO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog.makePresent(itemId)
|
showPlaylistDialog.makePresent(itemId)
|
||||||
},
|
},
|
||||||
onClickRemoveFromQueue = { _, _ -> },
|
onClickRemoveFromQueue = { _, _ -> },
|
||||||
|
|
@ -631,6 +630,7 @@ fun ArtistDetailsPage(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -133,7 +132,7 @@ fun SeriesDetails(
|
||||||
viewModel.setFavorite(itemId, favorite, null)
|
viewModel.setFavorite(itemId, favorite, null)
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog = Optional.present(itemId)
|
showPlaylistDialog = Optional.present(itemId)
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -307,6 +306,7 @@ fun SeriesDetails(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog.makeAbsent()
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.UseSerializers
|
import kotlinx.serialization.UseSerializers
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
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.api.PersonKind
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
|
|
@ -116,7 +115,7 @@ fun SeriesOverview(
|
||||||
viewModel.setFavorite(itemId, favorite, currentPosition.episodeRowIndex)
|
viewModel.setFavorite(itemId, favorite, currentPosition.episodeRowIndex)
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog = itemId
|
showPlaylistDialog = itemId
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -348,6 +347,7 @@ fun SeriesOverview(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog = null
|
showPlaylistDialog = null
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jellyfin.sdk.model.DateTime
|
import org.jellyfin.sdk.model.DateTime
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -177,7 +176,7 @@ fun HomePage(
|
||||||
onClickWatch = viewModel::setWatched,
|
onClickWatch = viewModel::setWatched,
|
||||||
onClickFavorite = viewModel::setFavorite,
|
onClickFavorite = viewModel::setFavorite,
|
||||||
onClickAddPlaylist = { itemId ->
|
onClickAddPlaylist = { itemId ->
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists()
|
||||||
showPlaylistDialog = itemId
|
showPlaylistDialog = itemId
|
||||||
},
|
},
|
||||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
|
@ -258,6 +257,7 @@ fun HomePage(
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog = null
|
showPlaylistDialog = null
|
||||||
},
|
},
|
||||||
|
onSearch = playlistViewModel::loadPlaylists,
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ class SearchViewModel
|
||||||
val sorted =
|
val sorted =
|
||||||
items.sortedWith(
|
items.sortedWith(
|
||||||
compareBy<BaseItem> { SearchRelevance.score(it, query) }
|
compareBy<BaseItem> { SearchRelevance.score(it, query) }
|
||||||
.thenBy { it.name ?: "" },
|
.thenBy { it.sortName },
|
||||||
)
|
)
|
||||||
_state.update { update.invoke(SearchResult.Success(sorted), it) }
|
_state.update { update.invoke(SearchResult.Success(sorted), it) }
|
||||||
} catch (ex: CancellationException) {
|
} catch (ex: CancellationException) {
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,20 @@ object SearchRelevance {
|
||||||
fun score(
|
fun score(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
query: String,
|
query: String,
|
||||||
|
): Int = score(item.name, item.type, query)
|
||||||
|
|
||||||
|
fun score(
|
||||||
|
name: String?,
|
||||||
|
type: BaseItemKind,
|
||||||
|
query: String,
|
||||||
): Int {
|
): 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())
|
val q = query.lowercase(Locale.getDefault())
|
||||||
|
|
||||||
REGEX_PATTERN.find(query)?.let { match ->
|
REGEX_PATTERN.find(query)?.let { match ->
|
||||||
val pattern = match.groupValues[1]
|
val pattern = match.groupValues[1]
|
||||||
val caseInsensitive = match.groupValues[2] == "i"
|
val caseInsensitive = match.groupValues[2] == "i"
|
||||||
return regexScore(item.name ?: "", pattern, caseInsensitive) + typeBonus(item.type)
|
return regexScore(name, pattern, caseInsensitive) + typeBonus(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
|
|
@ -26,7 +32,7 @@ object SearchRelevance {
|
||||||
name.contains(" $q") -> 200
|
name.contains(" $q") -> 200
|
||||||
name.contains(q) -> 300
|
name.contains(q) -> 300
|
||||||
else -> fuzzyScore(name, q)
|
else -> fuzzyScore(name, q)
|
||||||
} + typeBonus(item.type)
|
} + typeBonus(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun regexScore(
|
private fun regexScore(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue