feat: Added search WiP

This commit is contained in:
Stefan Schüller 2022-02-04 21:02:54 +01:00
parent c8af0c26e3
commit ed49946fb3
11 changed files with 358 additions and 15 deletions

View File

@ -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 *

View File

@ -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)

View File

@ -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()
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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."))
}
}
}

View File

@ -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 ?: ""
)
}
}
}
}

View File

@ -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")
}
}

View File

@ -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
}
)
}
}

View File

@ -10,4 +10,7 @@ sealed class VideoListEvent {
data class UpdateQuery(
val set: String?
): VideoListEvent()
data class UpdateSearchQuery(
val text: String?
): VideoListEvent()
}

View File

@ -0,0 +1,5 @@
package net.schueller.peertube.feature_video.presentation.video.states
data class VideoSearchState(
val text: String = ""
)