WIP 2 search
This commit is contained in:
parent
d815d717e2
commit
8730bbe796
@ -44,6 +44,7 @@
|
||||
<activity android:name=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".audio.recorder.ui.AudioRecorderActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".audio.audio_selector.ui.AudioSelectorActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".hashtags.ui.HashtagsActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<provider
|
||||
android:authorities="com.isolaatti.provider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
@ -11,7 +11,7 @@ import com.isolaatti.search.data.SearchHistoryEntity
|
||||
import com.isolaatti.settings.data.KeyValueDao
|
||||
import com.isolaatti.settings.data.KeyValueEntity
|
||||
|
||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class, SearchHistoryEntity::class], version = 6)
|
||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class, SearchHistoryEntity::class], version = 7)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun keyValueDao(): KeyValueDao
|
||||
abstract fun userInfoDao(): UserInfoDao
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
package com.isolaatti.hashtags.ui
|
||||
|
||||
import android.content.Context
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
|
||||
class HashtagsActivity : IsolaattiBaseActivity() {
|
||||
companion object {
|
||||
fun startActivity(context: Context, hashtag: String? = null) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
package com.isolaatti.search
|
||||
|
||||
import com.isolaatti.connectivity.RetrofitClient
|
||||
import com.isolaatti.database.AppDatabase
|
||||
import com.isolaatti.search.data.SearchApi
|
||||
import com.isolaatti.search.data.SearchDao
|
||||
import com.isolaatti.search.data.SearchRepositoryImpl
|
||||
import com.isolaatti.search.domain.SearchRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@ -14,4 +18,14 @@ class Module {
|
||||
fun provideSearchApi(retrofitClient: RetrofitClient): SearchApi {
|
||||
return retrofitClient.client.create(SearchApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSearchDao(database: AppDatabase): SearchDao {
|
||||
return database.searchHistoryDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSearchRepository(searchApi: SearchApi, searchDao: SearchDao): SearchRepository {
|
||||
return SearchRepositoryImpl(searchApi, searchDao)
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,16 @@
|
||||
package com.isolaatti.search.data
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface SearchApi {
|
||||
@GET("Search/Quick")
|
||||
fun quickSearch(@Query("q") query: String)
|
||||
fun quickSearch(@Query("q") query: String): Call<SearchDto>
|
||||
|
||||
@GET("hashtags/trending")
|
||||
fun getTrendingHashtags(): Call<HashtagsDto>
|
||||
|
||||
@GET("Search/newestUsers")
|
||||
fun getNewestUsers(@Query("limit") limit: Int, @Query("after") after: Int?): Call<NewestUsersDto>
|
||||
}
|
||||
@ -7,15 +7,32 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
|
||||
@Dao
|
||||
interface SearchDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertTerm(searchTerm: SearchHistoryEntity)
|
||||
|
||||
@Delete
|
||||
fun deleteTerm(searchTerm: SearchHistoryEntity)
|
||||
@Query("SELECT COUNT(*) FROM search_history WHERE `query` = :query")
|
||||
fun itemCount(query: String): Int
|
||||
|
||||
@Query("SELECT * FROM search_history ORDER BY time DESC")
|
||||
fun getTerms(): LiveData<List<SearchHistoryEntity>>
|
||||
@Transaction
|
||||
fun insertTermTransaction(searchTerm: SearchHistoryEntity) {
|
||||
if(itemCount(searchTerm.query) > 0) {
|
||||
deleteTerm(searchTerm.query)
|
||||
}
|
||||
insertTerm(searchTerm)
|
||||
}
|
||||
|
||||
@Query("DELETE FROM search_history WHERE `query` = :searchTerm")
|
||||
fun deleteTerm(searchTerm: String)
|
||||
|
||||
@Query("SELECT *, `rowid` FROM search_history ORDER BY time DESC")
|
||||
fun getTerms(): List<SearchHistoryEntity>
|
||||
|
||||
@Query("""
|
||||
SELECT *, `rowid` FROM search_history WHERE `query` MATCH :searchQuery
|
||||
""")
|
||||
fun getTermsForQuery(searchQuery: String): List<SearchHistoryEntity>
|
||||
}
|
||||
15
app/src/main/java/com/isolaatti/search/data/SearchDto.kt
Normal file
15
app/src/main/java/com/isolaatti/search/data/SearchDto.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package com.isolaatti.search.data
|
||||
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
|
||||
|
||||
data class ProfileSearchDto(val id: Int, val name: String, val imageId: String?, val following: Boolean)
|
||||
data class SearchDto(
|
||||
val profiles: List<ProfileSearchDto>,
|
||||
val posts: List<FeedDto.PostDto>
|
||||
// TODO add the other types
|
||||
)
|
||||
|
||||
data class HashtagsDto(val result: List<String>)
|
||||
|
||||
data class NewestUsersDto(val result: List<ProfileSearchDto>)
|
||||
@ -1,10 +1,17 @@
|
||||
package com.isolaatti.search.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Fts4
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "search_history")
|
||||
@Fts4
|
||||
data class SearchHistoryEntity(
|
||||
@PrimaryKey val query: String,
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "rowid")
|
||||
val rowId: Int,
|
||||
val query: String,
|
||||
val time: Long
|
||||
)
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
package com.isolaatti.search.data
|
||||
|
||||
import android.util.Log
|
||||
import com.isolaatti.search.domain.SearchRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import retrofit2.awaitResponse
|
||||
|
||||
class SearchRepositoryImpl(private val searchApi: SearchApi, private val searchDao: SearchDao) : SearchRepository {
|
||||
companion object {
|
||||
const val LOG_TAG = "SearchRepositoryImpl"
|
||||
}
|
||||
override fun search(query: String): Flow<Resource<SearchDto>> = flow {
|
||||
searchDao.insertTermTransaction(SearchHistoryEntity(0, query, System.currentTimeMillis()))
|
||||
|
||||
try {
|
||||
val response = searchApi.quickSearch(query).awaitResponse()
|
||||
if(response.isSuccessful) {
|
||||
val dto = response.body()
|
||||
if(dto == null) {
|
||||
Log.e(LOG_TAG, "Emitting error, could not get body")
|
||||
emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||
return@flow
|
||||
}
|
||||
emit(Resource.Success(dto))
|
||||
|
||||
} else {
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Log.e(LOG_TAG, "Error searching: ${e.message}")
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSuggestions(query: String): Flow<Resource<List<SearchHistoryEntity>>> = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
if(query.isEmpty()) {
|
||||
emit(Resource.Success(searchDao.getTerms()))
|
||||
} else {
|
||||
emit(Resource.Success(searchDao.getTermsForQuery(query)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun removeSuggestion(query: String): Flow<Resource<Boolean>> = flow {
|
||||
searchDao.deleteTerm(query)
|
||||
emit(Resource.Success(true))
|
||||
}
|
||||
|
||||
override fun getTrendingHashtags(): Flow<Resource<HashtagsDto>> = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
try {
|
||||
val result = searchApi.getTrendingHashtags().awaitResponse()
|
||||
if(result.isSuccessful) {
|
||||
val dto = result.body()
|
||||
dto?.also { emit(Resource.Success(it)) }
|
||||
} else {
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Log.e(LOG_TAG, "Error getting trending hashtags: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNewestUsers(): Flow<Resource<NewestUsersDto>> = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
try {
|
||||
val result = searchApi.getNewestUsers(10, null).awaitResponse()
|
||||
|
||||
if(result.isSuccessful) {
|
||||
val dto = result.body()
|
||||
if(dto != null) {
|
||||
emit(Resource.Success(dto))
|
||||
}
|
||||
|
||||
} else {
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Log.e(LOG_TAG, "Error getting newest users: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,19 @@
|
||||
package com.isolaatti.search.domain
|
||||
|
||||
import com.isolaatti.search.data.HashtagsDto
|
||||
import com.isolaatti.search.data.NewestUsersDto
|
||||
import com.isolaatti.search.data.SearchDto
|
||||
import com.isolaatti.search.data.SearchHistoryEntity
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SearchRepository {
|
||||
fun search(query: String): Flow<Resource<SearchDto>>
|
||||
|
||||
fun getSuggestions(query: String): Flow<Resource<List<SearchHistoryEntity>>>
|
||||
|
||||
fun removeSuggestion(query: String): Flow<Resource<Boolean>>
|
||||
|
||||
fun getTrendingHashtags(): Flow<Resource<HashtagsDto>>
|
||||
fun getNewestUsers(): Flow<Resource<NewestUsersDto>>
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.isolaatti.search.presentation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.isolaatti.databinding.SearchSuggestionItemBinding
|
||||
import com.isolaatti.search.data.SearchHistoryEntity
|
||||
|
||||
class SearchSuggestionsAdapter(
|
||||
private val onItemClick: (item: SearchHistoryEntity) -> Unit = {},
|
||||
private val onItemDeleteClick: (item: SearchHistoryEntity) -> Unit = {}
|
||||
) : ListAdapter<SearchHistoryEntity, SearchSuggestionsAdapter.SearchSuggestionItemViewHolder>(itemCallback) {
|
||||
|
||||
inner class SearchSuggestionItemViewHolder(val searchSuggestionItemBinding: SearchSuggestionItemBinding) : ViewHolder(searchSuggestionItemBinding.root)
|
||||
|
||||
companion object {
|
||||
val itemCallback = object: DiffUtil.ItemCallback<SearchHistoryEntity>() {
|
||||
override fun areItemsTheSame(oldItem: SearchHistoryEntity, newItem: SearchHistoryEntity): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SearchHistoryEntity, newItem: SearchHistoryEntity): Boolean {
|
||||
return oldItem.query == newItem.query
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SearchSuggestionItemViewHolder {
|
||||
return SearchSuggestionItemViewHolder(SearchSuggestionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SearchSuggestionItemViewHolder, position: Int) {
|
||||
val searchSuggestion = getItem(position)
|
||||
|
||||
holder.searchSuggestionItemBinding.suggestionText.text = searchSuggestion.query
|
||||
|
||||
holder.searchSuggestionItemBinding.root.setOnClickListener { onItemClick(searchSuggestion) }
|
||||
holder.searchSuggestionItemBinding.deleteButton.setOnClickListener { onItemDeleteClick(searchSuggestion) }
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,110 @@
|
||||
package com.isolaatti.search.presentation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.search.data.SearchApi
|
||||
import com.isolaatti.search.data.SearchDao
|
||||
import com.isolaatti.search.data.HashtagsDto
|
||||
import com.isolaatti.search.data.NewestUsersDto
|
||||
import com.isolaatti.search.data.SearchDto
|
||||
import com.isolaatti.search.data.SearchHistoryEntity
|
||||
import com.isolaatti.search.domain.SearchRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor() : ViewModel() {
|
||||
fun search(query: String) {
|
||||
viewModelScope.launch {
|
||||
class SearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
|
||||
companion object {
|
||||
const val LOG_TAG = "SearchViewModel"
|
||||
}
|
||||
|
||||
val searchSuggestions: MutableLiveData<List<SearchHistoryEntity>> = MutableLiveData()
|
||||
val searchResults: MutableLiveData<SearchDto> = MutableLiveData()
|
||||
val trendingHashtags: MutableLiveData<HashtagsDto> = MutableLiveData()
|
||||
val newestUsers: MutableLiveData<NewestUsersDto> = MutableLiveData()
|
||||
|
||||
var searchQuery = ""
|
||||
|
||||
fun search() {
|
||||
Log.d(LOG_TAG, searchQuery)
|
||||
viewModelScope.launch {
|
||||
if(searchQuery.isNotEmpty()) {
|
||||
searchRepository.search(searchQuery).onEach {
|
||||
when(it) {
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Success -> {
|
||||
searchResults.postValue(it.data!!)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun getSuggestions(query: String) {
|
||||
searchQuery = query
|
||||
viewModelScope.launch {
|
||||
searchRepository.getSuggestions(query).onEach {
|
||||
when(it) {
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Success -> {
|
||||
searchSuggestions.postValue(it.data!!)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSuggestion(suggestion: SearchHistoryEntity) {
|
||||
viewModelScope.launch {
|
||||
searchRepository.removeSuggestion(suggestion.query).onEach { resource ->
|
||||
if(resource is Resource.Success && resource.data == true) {
|
||||
val searchSuggestionsMutable = searchSuggestions.value?.toMutableList()
|
||||
searchSuggestionsMutable?.remove(suggestion)
|
||||
|
||||
searchSuggestionsMutable?.also { searchSuggestions.postValue(it) }
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTendingHashtags() {
|
||||
viewModelScope.launch {
|
||||
searchRepository.getTrendingHashtags().onEach { resource ->
|
||||
when(resource) {
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Success -> {
|
||||
resource.data?.also { trendingHashtags.postValue(it) }
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNewestUsers() {
|
||||
viewModelScope.launch {
|
||||
searchRepository.getNewestUsers().onEach { resource ->
|
||||
when(resource) {
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Success -> {
|
||||
resource.data?.also { newestUsers.postValue(it) }
|
||||
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
package com.isolaatti.search.presentation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.load
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.databinding.UsersCarouselItemBinding
|
||||
import com.isolaatti.search.data.ProfileSearchDto
|
||||
import com.isolaatti.utils.UrlGen
|
||||
|
||||
class UserCarouselAdapter(
|
||||
private val onProfileClick: (profileId: Int) -> Unit = {}
|
||||
) : ListAdapter<ProfileSearchDto, UserCarouselAdapter.UserCarouselItemViewHolder>(itemCallback) {
|
||||
|
||||
companion object {
|
||||
val itemCallback = object: DiffUtil.ItemCallback<ProfileSearchDto>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ProfileSearchDto,
|
||||
newItem: ProfileSearchDto
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ProfileSearchDto,
|
||||
newItem: ProfileSearchDto
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
inner class UserCarouselItemViewHolder(val usersCarouselItemBinding: UsersCarouselItemBinding) : ViewHolder(usersCarouselItemBinding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserCarouselItemViewHolder {
|
||||
return UserCarouselItemViewHolder(UsersCarouselItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: UserCarouselItemViewHolder, position: Int) {
|
||||
val user = getItem(position)
|
||||
|
||||
holder.usersCarouselItemBinding.userCarouselName.text = user.name
|
||||
if(user.imageId != null) {
|
||||
holder.usersCarouselItemBinding.userCarouselImageView.load(UrlGen.imageUrl(user.imageId))
|
||||
} else {
|
||||
holder.usersCarouselItemBinding.userCarouselImageView.load(R.drawable.avatar)
|
||||
}
|
||||
|
||||
holder.usersCarouselItemBinding.userCarouselCard.setOnClickListener { onProfileClick(user.id) }
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,19 +1,76 @@
|
||||
package com.isolaatti.search.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.carousel.CarouselLayoutManager
|
||||
import com.google.android.material.carousel.UncontainedCarouselStrategy
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.databinding.FragmentSearchBinding
|
||||
import com.isolaatti.hashtags.ui.HashtagsActivity
|
||||
import com.isolaatti.profile.ui.ProfileActivity
|
||||
import com.isolaatti.search.data.HashtagsDto
|
||||
import com.isolaatti.search.data.NewestUsersDto
|
||||
import com.isolaatti.search.data.SearchDto
|
||||
import com.isolaatti.search.data.SearchHistoryEntity
|
||||
import com.isolaatti.search.presentation.SearchSuggestionsAdapter
|
||||
import com.isolaatti.search.presentation.SearchViewModel
|
||||
import com.isolaatti.search.presentation.UserCarouselAdapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SearchFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
const val LOG_TAG = "SearchFragment"
|
||||
}
|
||||
|
||||
lateinit var viewBinding: FragmentSearchBinding
|
||||
private val viewModel: SearchViewModel by viewModels()
|
||||
private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null
|
||||
private var newestUsersAdapter: UserCarouselAdapter? = null
|
||||
|
||||
private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer {
|
||||
searchSuggestionsAdapter?.submitList(it)
|
||||
}
|
||||
|
||||
private val searchResultsObserver: Observer<SearchDto> = Observer {
|
||||
Log.d(LOG_TAG, it.toString())
|
||||
}
|
||||
|
||||
private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer {
|
||||
Log.d(LOG_TAG, it.toString())
|
||||
|
||||
viewBinding.chipGroup.removeAllViews()
|
||||
it.result.forEach { hashtag ->
|
||||
viewBinding.chipGroup.addView(Chip(requireContext()).apply {
|
||||
text = "#$hashtag"
|
||||
setOnClickListener {
|
||||
requireContext().startActivity(Intent(requireContext(), HashtagsActivity::class.java))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private val newestUsersObserver: Observer<NewestUsersDto> = Observer {
|
||||
newestUsersAdapter?.submitList(it.result)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.loadTendingHashtags()
|
||||
viewModel.loadNewestUsers()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -24,23 +81,83 @@ class SearchFragment : Fragment() {
|
||||
return viewBinding.root
|
||||
}
|
||||
|
||||
private fun showResults(show: Boolean) {
|
||||
if(show) {
|
||||
viewBinding.viewAnimator.displayedChild = 1
|
||||
viewBinding.searchBar.menu.findItem(R.id.close_button)?.setVisible(true)
|
||||
} else {
|
||||
viewBinding.viewAnimator.displayedChild = 0
|
||||
viewModel.searchQuery = ""
|
||||
viewBinding.searchBar.setText("")
|
||||
viewBinding.searchBar.menu.findItem(R.id.close_button)?.setVisible(false)
|
||||
}
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupListeners()
|
||||
setupObservers()
|
||||
searchSuggestionsAdapter = SearchSuggestionsAdapter(
|
||||
onItemClick = {
|
||||
viewBinding.searchBar.setText(it.query)
|
||||
viewModel.searchQuery = it.query
|
||||
viewBinding.searchView.hide()
|
||||
viewModel.search()
|
||||
showResults(true)
|
||||
},
|
||||
onItemDeleteClick = {
|
||||
viewModel.deleteSuggestion(it)
|
||||
}
|
||||
)
|
||||
viewBinding.searchHistoryRecyclerView.apply {
|
||||
adapter = searchSuggestionsAdapter
|
||||
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
}
|
||||
|
||||
newestUsersAdapter = UserCarouselAdapter(
|
||||
onProfileClick = {
|
||||
ProfileActivity.startActivity(requireContext(), it)
|
||||
}
|
||||
)
|
||||
viewBinding.newUsersRecyclerView.apply {
|
||||
adapter = newestUsersAdapter
|
||||
layoutManager = CarouselLayoutManager(UncontainedCarouselStrategy())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
viewBinding.searchView.editText.doAfterTextChanged { searchText ->
|
||||
if(searchText != null) {
|
||||
viewModel.search(searchText.toString())
|
||||
viewModel.getSuggestions(searchText.toString())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
viewBinding.searchView.editText.setOnEditorActionListener { v, actionId, event ->
|
||||
Log.d(LOG_TAG, "actionId: $actionId; event: $event")
|
||||
viewBinding.searchBar.setText(viewBinding.searchView.text)
|
||||
viewBinding.searchView.hide()
|
||||
viewModel.search()
|
||||
showResults(true)
|
||||
true
|
||||
}
|
||||
|
||||
viewBinding.searchBar.setOnMenuItemClickListener {
|
||||
if(it.itemId == R.id.close_button) {
|
||||
showResults(false)
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
viewBinding.searchView.addTransitionListener { searchView, transitionState, transitionState2 -> }
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
|
||||
viewModel.searchSuggestions.observe(viewLifecycleOwner, searchSuggestionsObserver)
|
||||
viewModel.searchResults.observe(viewLifecycleOwner, searchResultsObserver)
|
||||
viewModel.trendingHashtags.observe(viewLifecycleOwner, trendingHashtagsObserver)
|
||||
viewModel.newestUsers.observe(viewLifecycleOwner, newestUsersObserver)
|
||||
}
|
||||
}
|
||||
19
app/src/main/res/drawable/avatar.xml
Normal file
19
app/src/main/res/drawable/avatar.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h512v512h-512z"/>
|
||||
<path
|
||||
android:pathData="M0,0H512V512H0V0Z"
|
||||
android:fillColor="#222D3A"/>
|
||||
<path
|
||||
android:pathData="M330.08,110.96C311.3,90.67 285.06,79.5 256.1,79.5C226.98,79.5 200.65,90.6 181.96,110.76C163.05,131.14 153.84,158.84 156.01,188.75C160.29,247.75 205.19,295.75 256.1,295.75C307,295.75 351.83,247.76 356.18,188.76C358.37,159.13 349.1,131.49 330.08,110.96Z"
|
||||
android:fillColor="#B3BAC0"/>
|
||||
<path
|
||||
android:pathData="M53.93,512H458.33C459,503 458.21,483.5 456.33,473.14C448.18,427.94 422.76,389.97 382.79,363.33C347.28,339.67 302.3,326.64 256.13,326.64C209.96,326.64 164.98,339.67 129.47,363.33C89.5,389.98 64.08,427.95 55.93,473.15C54.05,483.51 53.5,504.5 53.93,512Z"
|
||||
android:fillColor="#B3BAC0"/>
|
||||
</group>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/clock_rotate_left_solid.xml
Normal file
9
app/src/main/res/drawable/clock_rotate_left_solid.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M75,75L41,41C25.9,25.9 0,36.6 0,57.9L0,168c0,13.3 10.7,24 24,24L134.1,192c21.4,0 32.1,-25.9 17,-41l-30.8,-30.8C155,85.5 203,64 256,64c106,0 192,86 192,192s-86,192 -192,192c-40.8,0 -78.6,-12.7 -109.7,-34.4c-14.5,-10.1 -34.4,-6.6 -44.6,7.9s-6.6,34.4 7.9,44.6C151.2,495 201.7,512 256,512c141.4,0 256,-114.6 256,-256S397.4,0 256,0C185.3,0 121.3,28.7 75,75zM256,128c-13.3,0 -24,10.7 -24,24L232,256c0,6.4 2.5,12.5 7,17l72,72c9.4,9.4 24.6,9.4 33.9,0s9.4,-24.6 0,-33.9l-65,-65L279.9,152c0,-13.3 -10.7,-24 -24,-24z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/markdown.xml
Normal file
9
app/src/main/res/drawable/markdown.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="640dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="640"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M593.8,59.1L46.2,59.1C20.7,59.1 0,79.8 0,105.2v301.5c0,25.5 20.7,46.2 46.2,46.2h547.7c25.5,0 46.2,-20.7 46.1,-46.1L640,105.2c0,-25.4 -20.7,-46.1 -46.2,-46.1zM338.5,360.6L277,360.6v-120l-61.5,76.9 -61.5,-76.9v120L92.3,360.6L92.3,151.4h61.5l61.5,76.9 61.5,-76.9h61.5v209.2zM473.8,363.7L381.5,256L443,256L443,151.4h61.5L504.5,256L566,256z"/>
|
||||
</vector>
|
||||
@ -2,17 +2,112 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<ViewAnimator
|
||||
android:id="@+id/view_animator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.search.SearchBar$ScrollingViewBehavior">
|
||||
<!-- Screen content goes here. -->
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
<!-- Platform content -->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/platform_content_nested_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:orientation="vertical">
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/hashtags_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.Material3.CardView.Filled">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hashtags_card_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hashtags"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chip_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/hashtags_card_title">
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/chip_group"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/see_all"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/new_users_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.Material3.CardView.Filled"
|
||||
android:layout_marginTop="16dp">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/new_users_card_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/newest_profiles"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/new_users_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toBottomOf="@id/new_users_card_title"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/new_users_recycler_view"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/see_all"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<!-- Search results -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view_search_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</ViewAnimator>
|
||||
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
@ -20,7 +115,8 @@
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/searchbar_hint" />
|
||||
android:hint="@string/searchbar_hint"
|
||||
app:menu="@menu/search_bar_menu"/>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
|
||||
40
app/src/main/res/layout/search_suggestion_item.xml
Normal file
40
app/src/main/res/layout/search_suggestion_item.xml
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/history_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/clock_rotate_left_solid"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/delete_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/delete_button" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/suggestion_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/delete_button"
|
||||
app:layout_constraintEnd_toStartOf="@id/delete_button"
|
||||
app:layout_constraintStart_toEndOf="@+id/history_icon"
|
||||
app:layout_constraintTop_toTopOf="@+id/delete_button"
|
||||
tools:text="hello search" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/baseline_close_24"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
42
app/src/main/res/layout/users_carousel_item.xml
Normal file
42
app/src/main/res/layout/users_carousel_item.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.carousel.MaskableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
app:shapeAppearance="?attr/shapeAppearanceCornerLarge"
|
||||
android:layout_margin="8dp">
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/user_carousel_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
style="@style/Widget.Material3.CardView.Elevated">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ImageView
|
||||
android:id="@+id/user_carousel_image_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_carousel_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Name"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:ellipsize="end"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/on_surface"
|
||||
android:maxLines="1"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</com.google.android.material.carousel.MaskableFrameLayout>
|
||||
9
app/src/main/res/menu/search_bar_menu.xml
Normal file
9
app/src/main/res/menu/search_bar_menu.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/close_button"
|
||||
android:title="@string/close"
|
||||
android:icon="@drawable/baseline_close_24"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@ -198,4 +198,7 @@
|
||||
<string name="delete_notification">Delete notification</string>
|
||||
<string name="delete_notification_dialog_message">Remove this notification?</string>
|
||||
<string name="error_making_request">An error ocurred</string>
|
||||
<string name="hashtags">Hashtags</string>
|
||||
<string name="newest_profiles">Newest profiles</string>
|
||||
<string name="see_all">See all</string>
|
||||
</resources>
|
||||
Loading…
x
Reference in New Issue
Block a user