Redesign SearchScreen (#172)

This commit is contained in:
vfsfitvnm 2022-09-23 17:00:12 +02:00
parent 6a3b41ca28
commit 35e0070bda
16 changed files with 657 additions and 421 deletions

View file

@ -220,6 +220,9 @@ interface Database {
@Query("SELECT loudnessDb FROM Format WHERE songId = :songId")
fun loudnessDb(songId: String): Flow<Float?>
@Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query")
fun search(query: String): Flow<List<DetailedSong>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(format: Format)

View file

@ -11,6 +11,8 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen
import it.vfsfitvnm.vimusic.ui.screens.settings.SettingsScreen
import it.vfsfitvnm.vimusic.ui.views.PlaylistsTab
import it.vfsfitvnm.vimusic.ui.views.SongsTab
import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey

View file

@ -1,400 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.net.Uri
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Composable
fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) {
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val (colorPalette, typography) = LocalAppearance.current
val layoutDirection = LocalLayoutDirection.current
val paddingValues = WindowInsets.systemBars.asPaddingValues()
val timeIconPainter = painterResource(R.drawable.time)
val closeIconPainter = painterResource(R.drawable.close)
val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward)
val rippleIndication = rememberRipple(bounded = true)
var textFieldValue by rememberSaveable(
initialTextInput,
stateSaver = TextFieldValue.Saver
) {
mutableStateOf(
TextFieldValue(
text = initialTextInput,
selection = TextRange(initialTextInput.length)
)
)
}
val focusRequester = remember {
FocusRequester()
}
val searchSuggestionsResult by produceState<Result<List<String>?>?>(
initialValue = null,
key1 = textFieldValue
) {
value = if (textFieldValue.text.isNotEmpty()) {
withContext(Dispatchers.IO) {
YouTube.getSearchSuggestions(textFieldValue.text)
}
} else {
null
}
}
val history by remember(textFieldValue.text) {
Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new ->
old.size == new.size
}
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
val isOpenableUrl = remember(textFieldValue.text) {
listOf(
"https://www.youtube.com/watch?",
"https://music.youtube.com/watch?",
"https://m.youtube.com/watch?",
"https://www.youtube.com/playlist?",
"https://music.youtube.com/playlist?",
"https://m.youtube.com/playlist?",
"https://youtu.be/",
).any(textFieldValue.text::startsWith)
}
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
}
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(
start = paddingValues.calculateStartPadding(layoutDirection),
end = paddingValues.calculateEndPadding(layoutDirection),
top = paddingValues.calculateTopPadding(),
)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
BasicTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
},
textStyle = typography.m.medium,
singleLine = true,
maxLines = 1,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
if (textFieldValue.text.isNotEmpty()) {
onSearch(textFieldValue.text)
}
}
),
cursorBrush = SolidColor(colorPalette.text),
decorationBox = { innerTextField ->
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
pop()
focusRequester.freeFocus()
}
.padding(vertical = 8.dp)
.padding(horizontal = 16.dp)
.size(24.dp)
)
Box(
modifier = Modifier
.weight(1f)
) {
androidx.compose.animation.AnimatedVisibility(
visible = textFieldValue.text.isEmpty(),
enter = fadeIn(tween(100)),
exit = fadeOut(tween(100)),
) {
BasicText(
text = "Enter a song, an album, an artist name...",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = typography.m.secondary,
)
}
innerTextField()
}
Box(
modifier = Modifier
.clickable {
textFieldValue = TextFieldValue()
}
.padding(horizontal = 14.dp, vertical = 6.dp)
.background(
color = colorPalette.background1,
shape = CircleShape
)
.size(28.dp)
) {
Image(
painter = painterResource(R.drawable.close),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
modifier = Modifier
.align(Alignment.Center)
.size(14.dp)
)
}
}
},
modifier = Modifier
.weight(1f)
.focusRequester(focusRequester)
)
}
if (isOpenableUrl) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onUri(textFieldValue.text.toUri()) }
)
.fillMaxWidth()
.background(colorPalette.background1)
.padding(vertical = 16.dp, horizontal = 8.dp)
) {
Image(
painter = painterResource(R.drawable.link),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.textDisabled),
modifier = Modifier
.padding(horizontal = 8.dp)
.size(20.dp)
)
BasicText(
text = "Open URL",
style = typography.s.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f)
)
}
}
LazyColumn(
contentPadding = PaddingValues(
bottom = Dimensions.collapsedPlayer + paddingValues.calculateBottomPadding()
)
) {
items(
items = history,
key = SearchQuery::id
) { searchQuery ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onSearch(searchQuery.query) }
)
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp)
) {
Spacer(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(20.dp)
.paint(
painter = timeIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
BasicText(
text = searchQuery.query,
style = typography.s.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f)
)
Spacer(
modifier = Modifier
.clickable {
query {
Database.delete(searchQuery)
}
}
.padding(horizontal = 8.dp)
.size(20.dp)
.paint(
painter = closeIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
Spacer(
modifier = Modifier
.clickable {
textFieldValue = TextFieldValue(
text = searchQuery.query,
selection = TextRange(searchQuery.query.length)
)
}
.rotate(225f)
.padding(horizontal = 8.dp)
.size(20.dp)
.paint(
painter = arrowForwardIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
}
}
searchSuggestionsResult?.getOrNull()?.let { suggestions ->
items(items = suggestions) { suggestion ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onSearch(suggestion) }
)
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp)
) {
Spacer(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(20.dp)
)
BasicText(
text = suggestion,
style = typography.s.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f)
)
Spacer(
modifier = Modifier
.clickable {
textFieldValue = TextFieldValue(
text = suggestion,
selection = TextRange(suggestion.length)
)
}
.rotate(225f)
.padding(horizontal = 8.dp)
.size(22.dp)
.paint(
painter = arrowForwardIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
}
}
} ?: searchSuggestionsResult?.exceptionOrNull()?.let { throwable ->
item {
LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {}
}
}
}
}
}
}
}

View file

@ -0,0 +1,145 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.align
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun LibrarySearchTab(
textFieldValue: TextFieldValue,
onTextFieldValueChanged: (TextFieldValue) -> Unit,
viewModel: LibrarySearchTabViewModel = viewModel(
key = textFieldValue.text,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return LibrarySearchTabViewModel(textFieldValue.text) as T
}
}
)
) {
val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val thumbnailSize = Dimensions.thumbnails.song.px
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.fillMaxSize()
) {
item(
key = "header",
contentType = 0
) {
Header(
titleContent = {
BasicTextField(
value = textFieldValue,
onValueChange = onTextFieldValueChanged,
textStyle = typography.xxl.medium.align(TextAlign.End),
singleLine = true,
maxLines = 1,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
cursorBrush = SolidColor(colorPalette.text),
decorationBox = { innerTextField ->
Box {
androidx.compose.animation.AnimatedVisibility(
visible = textFieldValue.text.isEmpty(),
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200)),
modifier = Modifier
.align(Alignment.CenterEnd)
) {
BasicText(
text = "Enter a name",
maxLines = 1,
style = typography.xxl.secondary
)
}
innerTextField()
}
}
)
},
actionsContent = {
BasicText(
text = "Clear",
style = typography.xxs.medium,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = textFieldValue.text.isNotEmpty()) {
onTextFieldValueChanged(TextFieldValue())
}
.background(colorPalette.background2)
.padding(all = 8.dp)
.padding(horizontal = 8.dp)
)
}
)
}
items(
items = viewModel.items,
key = DetailedSong::id,
) { song ->
SongItem(
song = song,
thumbnailSize = thumbnailSize,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
menuContent = { InHistoryMediaItemMenu(song = song) },
modifier = Modifier
.animateItemPlacement()
)
}
}
}

View file

@ -0,0 +1,25 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.DetailedSong
import kotlinx.coroutines.launch
class LibrarySearchTabViewModel(text: String) : ViewModel() {
var items by mutableStateOf(emptyList<DetailedSong>())
private set
init {
if (text.isNotEmpty()) {
viewModelScope.launch {
Database.search("%$text%").collect {
items = it
}
}
}
}
}

View file

@ -0,0 +1,298 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.paint
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.align
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import kotlinx.coroutines.delay
@Composable
fun OnlineSearchTab(
textFieldValue: TextFieldValue,
onTextFieldValueChanged: (TextFieldValue) -> Unit,
isOpenableUrl: Boolean,
onSearch: (String) -> Unit,
onUri: () -> Unit,
viewModel: OnlineSearchTabViewModel = viewModel(
key = textFieldValue.text,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return OnlineSearchTabViewModel(textFieldValue.text) as T
}
}
)
) {
val (colorPalette, typography) = LocalAppearance.current
val timeIconPainter = painterResource(R.drawable.time)
val closeIconPainter = painterResource(R.drawable.close)
val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward)
val rippleIndication = rememberRipple(bounded = true)
val focusRequester = remember {
FocusRequester()
}
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
}
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.fillMaxSize()
) {
item(
key = "header",
contentType = 0
) {
Header(
titleContent = {
BasicTextField(
value = textFieldValue,
onValueChange = onTextFieldValueChanged,
textStyle = typography.xxl.medium.align(TextAlign.End),
singleLine = true,
maxLines = 1,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
if (textFieldValue.text.isNotEmpty()) {
onSearch(textFieldValue.text)
}
}
),
cursorBrush = SolidColor(colorPalette.text),
decorationBox = { innerTextField ->
Box {
androidx.compose.animation.AnimatedVisibility(
visible = textFieldValue.text.isEmpty(),
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200)),
modifier = Modifier
.align(Alignment.CenterEnd)
) {
BasicText(
text = "Enter a name",
maxLines = 1,
style = typography.xxl.secondary
)
}
innerTextField()
}
},
modifier = Modifier
.focusRequester(focusRequester)
)
},
actionsContent = {
BasicText(
text = if (isOpenableUrl) "Open url" else "Search",
style = typography.xxs.medium,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = textFieldValue.text.isNotEmpty()) {
if (isOpenableUrl) onUri() else onSearch(textFieldValue.text)
}
.background(colorPalette.background2)
.padding(all = 8.dp)
.padding(horizontal = 8.dp)
)
Spacer(
modifier = Modifier
.weight(1f)
)
BasicText(
text = "Clear",
style = typography.xxs.medium,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = textFieldValue.text.isNotEmpty()) {
onTextFieldValueChanged(TextFieldValue())
}
.background(colorPalette.background2)
.padding(all = 8.dp)
.padding(horizontal = 8.dp)
)
}
)
}
items(
items = viewModel.history,
key = SearchQuery::id
) { searchQuery ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onSearch(searchQuery.query) }
)
.fillMaxWidth()
.padding(all = 16.dp)
) {
Spacer(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(20.dp)
.paint(
painter = timeIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
BasicText(
text = searchQuery.query,
style = typography.s.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f)
)
Spacer(
modifier = Modifier
.clickable {
query {
Database.delete(searchQuery)
}
}
.padding(horizontal = 8.dp)
.size(20.dp)
.paint(
painter = closeIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
Spacer(
modifier = Modifier
.clickable {
onTextFieldValueChanged(
TextFieldValue(
text = searchQuery.query,
selection = TextRange(searchQuery.query.length)
)
)
}
.rotate(225f)
.padding(horizontal = 8.dp)
.size(20.dp)
.paint(
painter = arrowForwardIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
}
}
viewModel.suggestionsResult?.getOrNull()?.let { suggestions ->
items(items = suggestions) { suggestion ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onSearch(suggestion) }
)
.fillMaxWidth()
.padding(all = 16.dp)
) {
Spacer(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(20.dp)
)
BasicText(
text = suggestion,
style = typography.s.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f)
)
Spacer(
modifier = Modifier
.clickable {
onTextFieldValueChanged(
TextFieldValue(
text = suggestion,
selection = TextRange(suggestion.length)
)
)
}
.rotate(225f)
.padding(horizontal = 8.dp)
.size(22.dp)
.paint(
painter = arrowForwardIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
)
}
}
} ?: viewModel.suggestionsResult?.exceptionOrNull()?.let { throwable ->
item {
LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {}
}
}
}
}

View file

@ -0,0 +1,36 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
class OnlineSearchTabViewModel(text: String) : ViewModel() {
var history by mutableStateOf(emptyList<SearchQuery>())
private set
var suggestionsResult by mutableStateOf<Result<List<String>?>?>(null)
private set
init {
viewModelScope.launch {
Database.queries("%$text%").distinctUntilChanged { old, new ->
old.size == new.size
}.collect {
history = it
}
}
if (text.isNotEmpty()) {
viewModelScope.launch {
suggestionsResult = YouTube.getSearchSuggestions(text)
}
}
}
}

View file

@ -0,0 +1,89 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import android.net.Uri
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.core.net.toUri
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) {
val saveableStateHolder = rememberSaveableStateHolder()
val (tabIndex, onTabChanged) = rememberSaveable {
mutableStateOf(0)
}
val (textFieldValue, onTextFieldValueChanged) = rememberSaveable(
initialTextInput,
stateSaver = TextFieldValue.Saver
) {
mutableStateOf(
TextFieldValue(
text = initialTextInput,
selection = TextRange(initialTextInput.length)
)
)
}
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val isOpenableUrl = remember(textFieldValue.text) {
listOf(
"https://www.youtube.com/watch?",
"https://music.youtube.com/watch?",
"https://m.youtube.com/watch?",
"https://www.youtube.com/playlist?",
"https://music.youtube.com/playlist?",
"https://m.youtube.com/playlist?",
"https://youtu.be/",
).any(textFieldValue.text::startsWith)
}
Scaffold(
topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop,
tabIndex = tabIndex,
onTabChanged = onTabChanged,
tabColumnContent = { Item ->
Item(0, "Online", R.drawable.globe)
Item(1, "Library", R.drawable.library)
}
) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(currentTabIndex) {
when (currentTabIndex) {
0 -> OnlineSearchTab(
textFieldValue = textFieldValue,
onTextFieldValueChanged = onTextFieldValueChanged,
isOpenableUrl = isOpenableUrl,
onSearch = onSearch,
onUri = {
if (isOpenableUrl) {
onUri(textFieldValue.text.toUri())
}
}
)
1 -> LibrarySearchTab(
textFieldValue = textFieldValue,
onTextFieldValueChanged = onTextFieldValueChanged
)
}
}
}
}
}
}

View file

@ -14,9 +14,6 @@ import androidx.compose.ui.platform.LocalUriHandler
import it.vfsfitvnm.vimusic.BuildConfig
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.secondary

View file

@ -16,10 +16,6 @@ import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
import it.vfsfitvnm.vimusic.enums.ColorPaletteName
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey

View file

@ -22,10 +22,6 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.enums.CoilDiskCacheMaxSize
import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.coilDiskCacheMaxSizeKey
import it.vfsfitvnm.vimusic.utils.exoPlayerDiskCacheMaxSizeKey

View file

@ -33,11 +33,6 @@ import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.service.PlayerService
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.intent
import it.vfsfitvnm.vimusic.utils.isIgnoringBatteryOptimizations

View file

@ -20,10 +20,6 @@ import androidx.compose.ui.platform.LocalContext
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.persistentQueueKey
import it.vfsfitvnm.vimusic.utils.rememberPreference

View file

@ -1,4 +1,4 @@
package it.vfsfitvnm.vimusic.ui.screens
package it.vfsfitvnm.vimusic.ui.screens.settings
import androidx.compose.animation.*
import androidx.compose.foundation.*
@ -19,6 +19,7 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.components.themed.Switch
import it.vfsfitvnm.vimusic.ui.components.themed.ValueSelectorDialog
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.settings.*
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.*

View file

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M340.75,344.49c5.91,-20.7 9.82,-44.75 11.31,-67.84A4.41,4.41 0,0 0,347.6 272H276.54a4.43,4.43 0,0 0,-4.47 4.39v55.3a4.44,4.44 0,0 0,4.14 4.38,273.51 273.51,0 0,1 59,11.39A4.45,4.45 0,0 0,340.75 344.49Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M323.58,377.31a260.05,260.05 0,0 0,-46.6 -9.09,4.42 4.42,0 0,0 -4.91,4.29v65.24a4.47,4.47 0,0 0,6.76 3.7c15.9,-9.27 29,-24.84 40.84,-45.43 1.94,-3.36 4.89,-9.15 6.67,-12.69A4.29,4.29 0,0 0,323.58 377.31Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M235.29,368.4a256.85,256.85 0,0 0,-46.56 8.82c-2.64,0.76 -3.75,4.4 -2.55,6.79 1.79,3.56 4,8.11 5.89,11.51 13,23 26.84,37.5 41.24,45.93a4.47,4.47 0,0 0,6.76 -3.7V372.48A4.16,4.16 0,0 0,235.29 368.4Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M235.6,272H164.54a4.41,4.41 0,0 0,-4.46 4.64c1.48,23.06 5.37,47.16 11.26,67.84a4.46,4.46 0,0 0,5.59 3,272.2 272.2,0 0,1 59,-11.36 4.44,4.44 0,0 0,4.15 -4.38V276.4A4.43,4.43 0,0 0,235.6 272Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M277,143.78a235.8,235.8 0,0 0,46.5 -9.14,4.3 4.3,0 0,0 2.76,-6c-1.79,-3.57 -4.27,-8.68 -6.17,-12.09 -12.29,-22 -26.14,-37.35 -41.24,-46a4.48,4.48 0,0 0,-6.76 3.7v65.23A4.43,4.43 0,0 0,277 143.78Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M276.54,240H347.6a4.39,4.39 0,0 0,4.46 -4.58c-1.48,-22.77 -5.27,-47.8 -11.16,-68.22a4.46,4.46 0,0 0,-5.59 -2.95c-19,5.74 -38.79,10.43 -59.09,12a4.4,4.4 0,0 0,-4.15 4.32v55.11A4.4,4.4 0,0 0,276.54 240Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M233.31,70.56c-15.42,8.57 -29.17,24.43 -41.47,46.37 -1.91,3.41 -4.19,8.11 -6,11.67a4.31,4.31 0,0 0,2.76 6,225.42 225.42,0 0,0 46.54,9.17 4.43,4.43 0,0 0,4.91 -4.29V74.26A4.49,4.49 0,0 0,233.31 70.56Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M235.92,176.26c-20.3,-1.55 -40.11,-6.24 -59.09,-12a4.46,4.46 0,0 0,-5.59 2.95c-5.89,20.42 -9.68,45.45 -11.16,68.22a4.39,4.39 0,0 0,4.46 4.58H235.6a4.4,4.4 0,0 0,4.47 -4.34V180.58A4.4,4.4 0,0 0,235.92 176.26Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M414.39,97.61A224,224 0,1 0,97.61 414.39,224 224,0 1,0 414.39,97.61ZM176.6,430.85a219.08,219.08 0,0 1,-12.48 -19.66c-2,-3.69 -4.84,-9.26 -6.73,-13.13a7.29,7.29 0,0 0,-10.31 -3.16c-4.3,2.41 -10,5.72 -14.13,8.43a147.29,147.29 0,0 1,-23.57 -22.43,248.83 248.83,0 0,1 30.41,-18.36c1.86,-1 2.77,-2.14 2.18,-4.18a374.8,374.8 0,0 1,-14.09 -82.17,4.36 4.36,0 0,0 -4.3,-4.17H66.84a2,2 0,0 1,-2 -1.7A98.28,98.28 0,0 1,64 256a96.27,96.27 0,0 1,0.86 -14.29,2 2,0 0,1 2,-1.7H123.6c2.29,0 4.17,-1.32 4.29,-3.63a372.71,372.71 0,0 1,14 -81.83,4.36 4.36,0 0,0 -2.19,-5.11 260.63,260.63 0,0 1,-29.84 -17.9A169.82,169.82 0,0 1,133 108.74c4.08,2.68 9.4,5.71 13.66,8.11a7.89,7.89 0,0 0,11 -3.42c1.88,-3.87 4,-8.18 6.06,-11.88a221.93,221.93 0,0 1,12.54 -19.91A185,185 0,0 1,256 64c28.94,0 55.9,7 80.53,18.46a202.23,202.23 0,0 1,12 19c2.59,4.66 5.34,10.37 7.66,15.32a4.29,4.29 0,0 0,5.92 1.94c5.38,-2.91 11.21,-6.26 16.34,-9.63a171.36,171.36 0,0 1,23.2 23,244.89 244.89,0 0,1 -29.06,17.31 4.35,4.35 0,0 0,-2.18 5.12,348.68 348.68,0 0,1 13.85,81.4 4.33,4.33 0,0 0,4.3 4.12l56.62,-0.07a2,2 0,0 1,2 1.7,117.46 117.46,0 0,1 0,28.62 2,2 0,0 1,-2 1.72l-56.67,0a4.35,4.35 0,0 0,-4.3 4.17,367.4 367.4,0 0,1 -13.87,81.3 4.45,4.45 0,0 0,2.19 5.19c5,2.59 10.57,5.48 15.37,8.42s9.55,6.08 14.13,9.34a172.73,172.73 0,0 1,-23 22.93c-2.44,-1.61 -5.34,-3.44 -7.84,-4.94 -1.72,-1 -4.89,-2.77 -6.65,-3.76 -3.82,-2.14 -7.88,-0.54 -9.79,3.4s-4.83,9.59 -6.87,13.25a212.42,212.42 0,0 1,-12.35 19.53C310.91,442.37 284.94,448 256,448S201.23,442.37 176.6,430.85Z"/>
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M64,480H48a32,32 0,0 1,-32 -32V112A32,32 0,0 1,48 80H64a32,32 0,0 1,32 32V448A32,32 0,0 1,64 480Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M240,176a32,32 0,0 0,-32 -32H144a32,32 0,0 0,-32 32v28a4,4 0,0 0,4 4H236a4,4 0,0 0,4 -4Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M112,448a32,32 0,0 0,32 32h64a32,32 0,0 0,32 -32V418a2,2 0,0 0,-2 -2H114a2,2 0,0 0,-2 2Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M114,240L238,240A2,2 0,0 1,240 242L240,382A2,2 0,0 1,238 384L114,384A2,2 0,0 1,112 382L112,242A2,2 0,0 1,114 240z"/>
<path
android:fillColor="#FF000000"
android:pathData="M320,480H288a32,32 0,0 1,-32 -32V64a32,32 0,0 1,32 -32h32a32,32 0,0 1,32 32V448A32,32 0,0 1,320 480Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M495.89,445.45l-32.23,-340c-1.48,-15.65 -16.94,-27 -34.53,-25.31l-31.85,3c-17.59,1.67 -30.65,15.71 -29.17,31.36l32.23,340c1.48,15.65 16.94,27 34.53,25.31l31.85,-3C484.31,475.14 497.37,461.1 495.89,445.45Z"/>
</vector>