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=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
|
||||||
<activity android:name=".audio.recorder.ui.AudioRecorderActivity" 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=".audio.audio_selector.ui.AudioSelectorActivity" android:theme="@style/Theme.Isolaatti" />
|
||||||
|
<activity android:name=".hashtags.ui.HashtagsActivity" android:theme="@style/Theme.Isolaatti" />
|
||||||
<provider
|
<provider
|
||||||
android:authorities="com.isolaatti.provider"
|
android:authorities="com.isolaatti.provider"
|
||||||
android:name="androidx.core.content.FileProvider"
|
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.KeyValueDao
|
||||||
import com.isolaatti.settings.data.KeyValueEntity
|
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 class AppDatabase : RoomDatabase() {
|
||||||
abstract fun keyValueDao(): KeyValueDao
|
abstract fun keyValueDao(): KeyValueDao
|
||||||
abstract fun userInfoDao(): UserInfoDao
|
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
|
package com.isolaatti.search
|
||||||
|
|
||||||
import com.isolaatti.connectivity.RetrofitClient
|
import com.isolaatti.connectivity.RetrofitClient
|
||||||
|
import com.isolaatti.database.AppDatabase
|
||||||
import com.isolaatti.search.data.SearchApi
|
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.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@ -14,4 +18,14 @@ class Module {
|
|||||||
fun provideSearchApi(retrofitClient: RetrofitClient): SearchApi {
|
fun provideSearchApi(retrofitClient: RetrofitClient): SearchApi {
|
||||||
return retrofitClient.client.create(SearchApi::class.java)
|
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
|
package com.isolaatti.search.data
|
||||||
|
|
||||||
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface SearchApi {
|
interface SearchApi {
|
||||||
@GET("Search/Quick")
|
@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.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SearchDao {
|
interface SearchDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insertTerm(searchTerm: SearchHistoryEntity)
|
fun insertTerm(searchTerm: SearchHistoryEntity)
|
||||||
|
|
||||||
@Delete
|
@Query("SELECT COUNT(*) FROM search_history WHERE `query` = :query")
|
||||||
fun deleteTerm(searchTerm: SearchHistoryEntity)
|
fun itemCount(query: String): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM search_history ORDER BY time DESC")
|
@Transaction
|
||||||
fun getTerms(): LiveData<List<SearchHistoryEntity>>
|
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
|
package com.isolaatti.search.data
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Fts4
|
||||||
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@Entity(tableName = "search_history")
|
@Entity(tableName = "search_history")
|
||||||
|
@Fts4
|
||||||
data class SearchHistoryEntity(
|
data class SearchHistoryEntity(
|
||||||
@PrimaryKey val query: String,
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "rowid")
|
||||||
|
val rowId: Int,
|
||||||
|
val query: String,
|
||||||
val time: Long
|
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
|
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 com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface SearchRepository {
|
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
|
package com.isolaatti.search.presentation
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.isolaatti.search.data.SearchApi
|
import com.isolaatti.search.data.HashtagsDto
|
||||||
import com.isolaatti.search.data.SearchDao
|
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 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 kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchViewModel @Inject constructor() : ViewModel() {
|
class SearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
|
||||||
fun search(query: String) {
|
companion object {
|
||||||
viewModelScope.launch {
|
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
|
package com.isolaatti.search.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
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.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.SearchViewModel
|
||||||
|
import com.isolaatti.search.presentation.UserCarouselAdapter
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOG_TAG = "SearchFragment"
|
||||||
|
}
|
||||||
|
|
||||||
lateinit var viewBinding: FragmentSearchBinding
|
lateinit var viewBinding: FragmentSearchBinding
|
||||||
private val viewModel: SearchViewModel by viewModels()
|
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(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -24,23 +81,83 @@ class SearchFragment : Fragment() {
|
|||||||
return viewBinding.root
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupListeners()
|
setupListeners()
|
||||||
setupObservers()
|
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() {
|
private fun setupListeners() {
|
||||||
viewBinding.searchView.editText.doAfterTextChanged { searchText ->
|
viewBinding.searchView.editText.doAfterTextChanged { searchText ->
|
||||||
if(searchText != null) {
|
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() {
|
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
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
|
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
|
||||||
<androidx.core.widget.NestedScrollView
|
<ViewAnimator
|
||||||
|
android:id="@+id/view_animator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="com.google.android.material.search.SearchBar$ScrollingViewBehavior">
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||||
<!-- Screen content goes here. -->
|
<!-- 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>
|
</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
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
@ -20,7 +115,8 @@
|
|||||||
android:id="@+id/search_bar"
|
android:id="@+id/search_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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>
|
</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">Delete notification</string>
|
||||||
<string name="delete_notification_dialog_message">Remove this notification?</string>
|
<string name="delete_notification_dialog_message">Remove this notification?</string>
|
||||||
<string name="error_making_request">An error ocurred</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>
|
</resources>
|
||||||
Loading…
x
Reference in New Issue
Block a user