feat: Added search WiP
This commit is contained in:
parent
c8af0c26e3
commit
ed49946fb3
11
CI_LOCAL.md
11
CI_LOCAL.md
|
@ -1,4 +1,5 @@
|
|||
## Required Gitlab CI Variables
|
||||
# Required Gitlab CI Variables
|
||||
```
|
||||
github_token # git hub token to push release to github
|
||||
GITLAB_TOKEN # token from gitlab user so the version bump can be commited
|
||||
google_play_service_account_api_key_json # google play store json
|
||||
|
@ -6,8 +7,8 @@ signing_jks_file_hex # We store this binary file in a variable as hex with this
|
|||
signing_key_alias # Alias name of signing key
|
||||
signing_key_password # Key password
|
||||
signing_keystore_password # keystore password
|
||||
|
||||
## Testing CI locally
|
||||
```
|
||||
#### Testing CI locally
|
||||
|
||||
```
|
||||
cd ${repo}
|
||||
|
@ -22,10 +23,10 @@ sudo docker run --rm -v "$(pwd):/build/project" -w "/build/project" -it thorium
|
|||
sudo docker run --rm -v "$(pwd):/build/project" -w "/build/project" -it thorium bundle exec fastlane buildRelease
|
||||
```
|
||||
|
||||
# warning running this on your local repo may create files owned by root because of docker for example the build dir.
|
||||
#### warning running this on your local repo may create files owned by root because of docker for example the build dir.
|
||||
These have to be removed with sudo
|
||||
|
||||
# Update fastlane
|
||||
#### Update fastlane
|
||||
```
|
||||
sudo docker run --rm -v "$(pwd):/build/project" -w "/build/project" -it thorium bundle update
|
||||
sudo chown -R myuser *
|
||||
|
|
2
TODO.md
2
TODO.md
|
@ -34,4 +34,4 @@ Issues:
|
|||
- playback rotate on click doesn't re-hide buttons
|
||||
- Explore list is memory intensive, leak??
|
||||
- Access Token refresh circular injection problem
|
||||
|
||||
- app crashes when clicking items in background list while player is visible (minimode)
|
||||
|
|
|
@ -21,6 +21,10 @@ class VideoRepositoryImpl @Inject constructor(
|
|||
return api.getVideos(start, count, sort, nsfw, filter, languages).toVideoList()
|
||||
}
|
||||
|
||||
override suspend fun searchVideos(start: Int, count: Int, sort: String?,nsfw: String?, searchQuery: String?, filter: String?, languages: Set<String?>?): List<Video> {
|
||||
return api.searchVideos(start, count, sort, nsfw, searchQuery, filter, languages).toVideoList()
|
||||
}
|
||||
|
||||
override suspend fun getVideoByUuid(uuid: String): Video {
|
||||
return api.getVideo(uuid).toVideo()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ interface VideoRepository {
|
|||
|
||||
suspend fun getVideos(start: Int,count: Int,sort: String?,nsfw: String?,filter: String?,languages: Set<String?>?): List<Video>
|
||||
|
||||
suspend fun searchVideos(start: Int, count: Int, sort: String?,nsfw: String?, searchQuery: String?, filter: String?, languages: Set<String?>?): List<Video>
|
||||
|
||||
suspend fun getOverviewVideos(page: Int): List<Overview>
|
||||
|
||||
suspend fun getVideoByUuid(uuid: String): Video
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package net.schueller.peertube.feature_video.domain.source
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import net.schueller.peertube.common.Constants.VIDEOS_API_PAGE_SIZE
|
||||
import net.schueller.peertube.common.Constants.VIDEOS_API_START_INDEX
|
||||
import net.schueller.peertube.feature_video.domain.model.Video
|
||||
import net.schueller.peertube.feature_video.domain.repository.VideoRepository
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class SearchPagingSource (
|
||||
private val repository: VideoRepository,
|
||||
private val sort: String?,
|
||||
private val nsfw: String?,
|
||||
private val searchQuery: String?,
|
||||
private val filter: String?,
|
||||
private val languages: Set<String?>?
|
||||
) : PagingSource<Int, Video>() {
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Video> {
|
||||
val position = params.key ?: VIDEOS_API_START_INDEX
|
||||
|
||||
val offset = if (params.key != null) ((position) * VIDEOS_API_PAGE_SIZE) else VIDEOS_API_START_INDEX
|
||||
|
||||
return try {
|
||||
val videos = repository.searchVideos(offset, params.loadSize, sort, nsfw, searchQuery, filter, languages)
|
||||
|
||||
val nextKey = if (videos.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
// initial load size = 3 * NETWORK_PAGE_SIZE
|
||||
// ensure we're not requesting duplicating items, at the 2nd request
|
||||
position + (params.loadSize / VIDEOS_API_PAGE_SIZE)
|
||||
}
|
||||
LoadResult.Page(
|
||||
data = videos,
|
||||
prevKey = if (position == VIDEOS_API_START_INDEX) null else position - 1,
|
||||
nextKey = nextKey
|
||||
)
|
||||
|
||||
} catch (exception: HttpException) {
|
||||
Log.v("video1", exception.localizedMessage ?: "An unexpected error occurred")
|
||||
return LoadResult.Error(exception)
|
||||
} catch (exception: IOException) {
|
||||
Log.v("video1", "Couldn't reach server. Check your internet connection.")
|
||||
return LoadResult.Error(exception)
|
||||
}
|
||||
}
|
||||
// The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load
|
||||
override fun getRefreshKey(state: PagingState<Int, Video>): Int? {
|
||||
// We need to get the previous key (or next key if previous is null) of the page
|
||||
// that was closest to the most recently accessed index.
|
||||
// Anchor position is the most recently accessed index
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package net.schueller.peertube.feature_video.domain.use_case
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import net.schueller.peertube.common.Resource
|
||||
import net.schueller.peertube.feature_video.domain.model.Video
|
||||
import net.schueller.peertube.feature_video.domain.repository.VideoRepository
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchVideoListUseCase @Inject constructor(
|
||||
private val repository: VideoRepository
|
||||
) {
|
||||
operator fun invoke(start: Int,count: Int,sort: String?,nsfw: String?, searchQuery: String?,filter: String?,languages: Set<String?>?): Flow<Resource<List<Video>>> = flow {
|
||||
try {
|
||||
emit(Resource.Loading<List<Video>>())
|
||||
val videos = repository.searchVideos(start, count, sort, nsfw, searchQuery, filter, languages)
|
||||
emit(Resource.Success<List<Video>>(videos))
|
||||
} catch(e: HttpException) {
|
||||
emit(Resource.Error<List<Video>>(e.localizedMessage ?: "An unexpected error occurred"))
|
||||
} catch(e: IOException) {
|
||||
emit(Resource.Error<List<Video>>("Couldn't reach server. Check your internet connection."))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -10,16 +10,20 @@ import androidx.paging.Pager
|
|||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.jamal.composeprefs.ui.ifNotNullThen
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import net.schueller.peertube.common.Constants.VIDEOS_API_PAGE_SIZE
|
||||
import net.schueller.peertube.feature_server_address.presentation.address_add_edit.AddressTextFieldState
|
||||
import net.schueller.peertube.feature_video.data.remote.auth.Session
|
||||
import net.schueller.peertube.feature_video.domain.model.Video
|
||||
import net.schueller.peertube.feature_video.domain.repository.VideoRepository
|
||||
import net.schueller.peertube.feature_video.domain.source.SearchPagingSource
|
||||
import net.schueller.peertube.feature_video.domain.source.VideoPagingSource
|
||||
import net.schueller.peertube.feature_video.presentation.video.events.*
|
||||
import net.schueller.peertube.feature_video.presentation.video.states.VideoListState
|
||||
import net.schueller.peertube.feature_video.presentation.video.states.VideoSearchState
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
@ -31,6 +35,9 @@ class VideoListViewModel @Inject constructor(
|
|||
private val _state = mutableStateOf(VideoListState())
|
||||
val state: State<VideoListState> = _state
|
||||
|
||||
private val _videoSearchState = mutableStateOf(VideoSearchState())
|
||||
val videoSearchState: State<VideoSearchState> = _videoSearchState
|
||||
|
||||
private val _eventFlow = MutableSharedFlow<UiEvent>()
|
||||
val eventFlow = _eventFlow.asSharedFlow()
|
||||
|
||||
|
@ -42,7 +49,9 @@ class VideoListViewModel @Inject constructor(
|
|||
getVideos()
|
||||
}
|
||||
|
||||
private fun getVideos() {
|
||||
var noSearchResults = false
|
||||
|
||||
fun getVideos() {
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
PagingConfig(
|
||||
|
@ -50,14 +59,26 @@ class VideoListViewModel @Inject constructor(
|
|||
maxSize = 100
|
||||
)
|
||||
) {
|
||||
VideoPagingSource(
|
||||
repository,
|
||||
_state.value.sort,
|
||||
_state.value.nsfw,
|
||||
_state.value.filter,
|
||||
_state.value.languages
|
||||
)
|
||||
if (videoSearchState.value.text.isNotEmpty()) {
|
||||
SearchPagingSource(
|
||||
repository,
|
||||
_state.value.sort,
|
||||
_state.value.nsfw,
|
||||
videoSearchState.value.text,
|
||||
_state.value.filter,
|
||||
_state.value.languages
|
||||
)
|
||||
} else {
|
||||
VideoPagingSource(
|
||||
repository,
|
||||
_state.value.sort,
|
||||
_state.value.nsfw,
|
||||
_state.value.filter,
|
||||
_state.value.languages
|
||||
)
|
||||
}
|
||||
}.flow.cachedIn(viewModelScope).collect {
|
||||
noSearchResults = it.equals(null)
|
||||
_videos.value = it
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +149,13 @@ class VideoListViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
}
|
||||
is VideoListEvent.UpdateSearchQuery -> {
|
||||
viewModelScope.launch {
|
||||
_videoSearchState.value = videoSearchState.value.copy(
|
||||
text = event.text ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
package net.schueller.peertube.feature_video.presentation.video.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import net.schueller.peertube.feature_video.presentation.video.VideoListViewModel
|
||||
import net.schueller.peertube.feature_video.presentation.video.events.VideoListEvent
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun VideoSearch(
|
||||
hide: () -> Unit = {},
|
||||
videoListViewModel: VideoListViewModel = hiltViewModel()
|
||||
) {
|
||||
val videoSearchString = videoListViewModel.videoSearchState.value
|
||||
|
||||
SearchBar(
|
||||
searchText = videoSearchString.text,
|
||||
placeholderText = "Search",
|
||||
onSearchTextChanged = {
|
||||
videoListViewModel.onEvent(VideoListEvent.UpdateSearchQuery(it))
|
||||
},
|
||||
onClearClick = {
|
||||
videoListViewModel.onEvent(VideoListEvent.UpdateSearchQuery(null))
|
||||
videoListViewModel.getVideos()
|
||||
},
|
||||
onNavigateBack = {
|
||||
hide()
|
||||
},
|
||||
matchesFound = !videoListViewModel.noSearchResults
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalComposeUiApi
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
searchText: String,
|
||||
placeholderText: String = "",
|
||||
onSearchTextChanged: (String) -> Unit = {},
|
||||
onClearClick: () -> Unit = {},
|
||||
onNavigateBack: () -> Unit = {},
|
||||
matchesFound: Boolean,
|
||||
results: @Composable () -> Unit = {}
|
||||
) {
|
||||
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
|
||||
SearchBar(
|
||||
searchText,
|
||||
placeholderText,
|
||||
onSearchTextChanged,
|
||||
onClearClick,
|
||||
onNavigateBack
|
||||
)
|
||||
|
||||
if (!matchesFound) {
|
||||
if (searchText.isNotEmpty()) {
|
||||
NoSearchResults()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalComposeUiApi
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
searchText: String,
|
||||
placeholderText: String = "",
|
||||
onSearchTextChanged: (String) -> Unit = {},
|
||||
onClearClick: () -> Unit = {},
|
||||
onNavigateBack: () -> Unit = {},
|
||||
videoListViewModel: VideoListViewModel = hiltViewModel()
|
||||
) {
|
||||
var showClearButton by remember { mutableStateOf(false) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
|
||||
|
||||
TopAppBar(title = { Text("") }, navigationIcon = {
|
||||
IconButton(onClick = { onNavigateBack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Arrow Back"
|
||||
)
|
||||
}
|
||||
}, actions = {
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
.onFocusChanged { focusState ->
|
||||
showClearButton = (focusState.isFocused)
|
||||
}
|
||||
.focusRequester(focusRequester),
|
||||
value = searchText,
|
||||
onValueChange = onSearchTextChanged,
|
||||
placeholder = {
|
||||
Text(text = placeholderText)
|
||||
},
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
backgroundColor = Color.Transparent,
|
||||
cursorColor = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
),
|
||||
trailingIcon = {
|
||||
AnimatedVisibility(
|
||||
visible = showClearButton,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
IconButton(onClick = { onClearClick() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Close"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
videoListViewModel.getVideos()
|
||||
keyboardController?.hide()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun NoSearchResults() {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = CenterHorizontally
|
||||
) {
|
||||
|
||||
Text("No matches found")
|
||||
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package net.schueller.peertube.feature_video.presentation.video.components.appBarTop
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
@ -10,7 +13,9 @@ import coil.annotation.ExperimentalCoilApi
|
|||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.feature_video.presentation.me.MeViewModel
|
||||
import net.schueller.peertube.feature_video.presentation.me.components.MeAvatar
|
||||
import net.schueller.peertube.feature_video.presentation.video.components.VideoSearch
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||
@ExperimentalCoilApi
|
||||
@Composable
|
||||
fun TopAppBarComponent(
|
||||
|
@ -18,11 +23,23 @@ fun TopAppBarComponent(
|
|||
modifier: Modifier,
|
||||
meViewModel: MeViewModel = hiltViewModel()
|
||||
) {
|
||||
var searchBarVisible by remember { mutableStateOf(false) }
|
||||
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = { Text(text = "AppBar") },
|
||||
// color = Color.White,
|
||||
actions = {
|
||||
IconButton(
|
||||
modifier = Modifier,
|
||||
onClick = {
|
||||
searchBarVisible = true
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Search,
|
||||
contentDescription = "Search"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
navController.navigate("address_list") {
|
||||
// Pop up to the start destination of the graph to
|
||||
|
@ -53,4 +70,13 @@ fun TopAppBarComponent(
|
|||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (searchBarVisible) {
|
||||
VideoSearch(
|
||||
hide = {
|
||||
searchBarVisible = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,4 +10,7 @@ sealed class VideoListEvent {
|
|||
data class UpdateQuery(
|
||||
val set: String?
|
||||
): VideoListEvent()
|
||||
data class UpdateSearchQuery(
|
||||
val text: String?
|
||||
): VideoListEvent()
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package net.schueller.peertube.feature_video.presentation.video.states
|
||||
|
||||
data class VideoSearchState(
|
||||
val text: String = ""
|
||||
)
|
Loading…
Reference in New Issue