Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add saerch history #143

Merged
merged 13 commits into from
Sep 15, 2024

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import ir.jaamebaade.jaamebaade_client.repository.HistoryRepository
import ir.jaamebaade.jaamebaade_client.repository.SearchHistoryRepository
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
Expand Down Expand Up @@ -204,4 +205,10 @@ object AppModule {
fun provideHistoryRepository(appDatabase: AppDatabase): HistoryRepository {
return HistoryRepository(appDatabase)
}

@Provides
@Singleton
fun provideSearchHistoryRepository(appDatabase: AppDatabase): SearchHistoryRepository {
return SearchHistoryRepository(appDatabase)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ir.jaamebaade.jaamebaade_client.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ir.jaamebaade.jaamebaade_client.model.SearchHistoryRecord
import kotlinx.coroutines.flow.Flow

@Dao
interface SearchHistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSearchHistoryRecord(searchHistoryRecord: SearchHistoryRecord)

@Query("SELECT * FROM search_history ORDER BY timestamp DESC")
fun getSearchHistoryRecords(): Flow<List<SearchHistoryRecord>>

@Query("DELETE FROM search_history WHERE id = :id")
fun removeSearchHistoryRecord(id: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ir.jaamebaade.jaamebaade_client.dao.HighlightDao
import ir.jaamebaade.jaamebaade_client.dao.HistoryItemDao
import ir.jaamebaade.jaamebaade_client.dao.PoemDao
import ir.jaamebaade.jaamebaade_client.dao.PoetDao
import ir.jaamebaade.jaamebaade_client.dao.SearchHistoryDao
import ir.jaamebaade.jaamebaade_client.dao.VerseDao
import ir.jaamebaade.jaamebaade_client.model.Bookmark
import ir.jaamebaade.jaamebaade_client.model.Category
Expand All @@ -18,15 +19,17 @@ import ir.jaamebaade.jaamebaade_client.model.Highlight
import ir.jaamebaade.jaamebaade_client.model.HistoryRecord
import ir.jaamebaade.jaamebaade_client.model.Poem
import ir.jaamebaade.jaamebaade_client.model.Poet
import ir.jaamebaade.jaamebaade_client.model.SearchHistoryRecord
import ir.jaamebaade.jaamebaade_client.model.Verse

@Database(
entities = [Poet::class, Category::class, Poem::class,
Verse::class, Highlight::class, Bookmark::class, Comment::class,
HistoryRecord::class],
version = 2,
HistoryRecord::class, SearchHistoryRecord::class],
version = 3,
autoMigrations = [
AutoMigration(from = 1, to = 2)
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
]
)
abstract class AppDatabase : RoomDatabase() {
Expand All @@ -38,4 +41,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun bookmarkDao(): BookmarkDao
abstract fun commentDao(): CommentDao
abstract fun historyDao(): HistoryItemDao
abstract fun searchHistoryDao(): SearchHistoryDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ir.jaamebaade.jaamebaade_client.model

import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey

@Entity(tableName = "search_history",
indices = [Index(value = ["query"], unique = true)])
data class SearchHistoryRecord(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val query: String,
val timestamp: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ir.jaamebaade.jaamebaade_client.repository

import ir.jaamebaade.jaamebaade_client.database.AppDatabase
import ir.jaamebaade.jaamebaade_client.model.SearchHistoryRecord
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject


class SearchHistoryRepository @Inject constructor(
appDatabase: AppDatabase
) {
private val db = appDatabase
private val searchHistoryDao = db.searchHistoryDao()

suspend fun insertSearchHistoryRecord(searchHistoryRecord: SearchHistoryRecord) {
searchHistoryDao.insertSearchHistoryRecord(searchHistoryRecord)
}

fun getSearchHistoryRecords(): Flow<List<SearchHistoryRecord>> {
return searchHistoryDao.getSearchHistoryRecords()
}

fun removeSearchHistoryRecord(historyRecord: SearchHistoryRecord) {
searchHistoryDao.removeSearchHistoryRecord(historyRecord.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,42 @@ fun SearchScreen(

val results = searchViewModel.results
val poets = searchViewModel.allPoets

val showingSearchHistory = searchViewModel.showingSearchHistory.collectAsState()
Column(modifier = modifier) {
SearchBar(modifier = Modifier.fillMaxWidth(),
SearchBar(
modifier = Modifier.fillMaxWidth(),
poets = poets.collectAsState().value,
onSearchFilterChanged = { searchViewModel.poetFilter = it },
onSearchQueryChanged = { searchViewModel.query = it }) {
searchHistoryRecords = showingSearchHistory.value,
onSearchFilterChanged = {
searchViewModel.poetFilter = it
},
onSearchQueryChanged = { it ->
searchViewModel.query = it
searchViewModel.onQueryChanged()
},
onSearchHistoryRecordClick = { historyItem ->
searchViewModel.query = historyItem.query
searchStatus = Status.LOADING
searchViewModel.search {
searchStatus = Status.SUCCESS
}
},
onSearchHistoryRecordDeleteClick = { historyItem ->
searchViewModel.deleteHistoryItem(historyItem)
},
onSearchClearClick = {
searchViewModel.clearSearch()
searchStatus = Status.NOT_STARTED
}

) {
if (it.length > 2) {
searchStatus = Status.LOADING
searchViewModel.search(callBack = { searchStatus = Status.SUCCESS })
}
}

SearchResults(
results = results,
searchQuery = searchViewModel.query,
Expand All @@ -62,6 +88,7 @@ fun SearchScreen(
}
}


@Composable
fun SearchResults(
results: List<VersePoemCategoriesPoet>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package ir.jaamebaade.jaamebaade_client.view.components

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -9,10 +13,15 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material3.DropdownMenu
Expand All @@ -33,25 +42,34 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import ir.jaamebaade.jaamebaade_client.R
import ir.jaamebaade.jaamebaade_client.model.Poet
import ir.jaamebaade.jaamebaade_client.model.SearchHistoryRecord

@Composable
fun SearchBar(
modifier: Modifier,
poets: List<Poet>,
searchHistoryRecords: List<SearchHistoryRecord> = emptyList(),
onSearchFilterChanged: (Poet?) -> Unit,
onSearchQueryChanged: (String) -> Unit,
onSearchHistoryRecordClick: ((SearchHistoryRecord) -> Unit)? = null,
onSearchHistoryRecordDeleteClick: ((SearchHistoryRecord) -> Unit)? = null,
onSearchClearClick: (String) -> Unit,
onSearchQueryIconClicked: (String) -> Unit,
) {
var query by rememberSaveable { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
var selectedPoetIndex by rememberSaveable { mutableStateOf<Int?>(null) }
var isSearchIconClicked by remember { mutableStateOf(false) }

val keyboardController = LocalSoftwareKeyboardController.current


Column {
TextField(
value = query,
Expand All @@ -63,19 +81,34 @@ fun SearchBar(
.fillMaxWidth()
.background(Color.Transparent),
trailingIcon = {
IconButton(onClick = {
onSearchQueryIconClicked(query)
keyboardController?.hide()
}) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search"
)
if (!isSearchIconClicked) {
IconButton(onClick = {
onSearchQueryIconClicked(query)
keyboardController?.hide()
isSearchIconClicked = true
}) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.SEARCH)
)
}
} else {
IconButton(onClick = {
onSearchClearClick(query)
query = ""
isSearchIconClicked = false
}) {
Icon(
imageVector = Icons.Default.Close, contentDescription = stringResource(
R.string.CLOSE
)
)
}
}
},
label = {
Text(
"جست‌وجو",
stringResource(R.string.SEARCH),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface
)
Expand All @@ -84,6 +117,7 @@ fun SearchBar(
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = {
onSearchQueryIconClicked(query)
isSearchIconClicked = true
keyboardController?.hide()
}),
)
Expand Down Expand Up @@ -135,7 +169,12 @@ fun SearchBar(
poets.forEachIndexed { index, poet ->
HorizontalDivider()
DropdownMenuItem(
text = { Text(poet.name, style = MaterialTheme.typography.labelMedium) },
text = {
Text(
poet.name,
style = MaterialTheme.typography.labelMedium
)
},
onClick = {
onSearchFilterChanged(poet)
selectedPoetIndex = index
Expand All @@ -145,5 +184,93 @@ fun SearchBar(
}
}
}

AnimatedVisibility(
visible = !isSearchIconClicked,
enter = fadeIn(),
exit = fadeOut()
) {
SearchHistoryList(
searchHistoryRecords = searchHistoryRecords,
onSearchHistoryRecordClick = { historyItem ->
onSearchHistoryRecordClick?.invoke(historyItem)
query = historyItem.query
isSearchIconClicked = true
},
onSearchHistoryRecordDelete = { historyItem ->
onSearchHistoryRecordDeleteClick?.invoke(historyItem)
},
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(8.dp)

)


}


}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchHistoryList(
modifier: Modifier = Modifier,
searchHistoryRecords: List<SearchHistoryRecord>,
onSearchHistoryRecordClick: (SearchHistoryRecord) -> Unit,
onSearchHistoryRecordDelete: (SearchHistoryRecord) -> Unit
) {

LazyColumn(
modifier = modifier
.fillMaxWidth()
MuhammadKhosravi marked this conversation as resolved.
Show resolved Hide resolved
) {
item {
Text(
text = stringResource(R.string.RECENT_SEARCHES),
style = MaterialTheme.typography.titleMedium
)
}

items(items = searchHistoryRecords, key = { it.id }) { historyItem ->
SearchHistoryRecordItem(
modifier = Modifier.animateItemPlacement(),
historyItem = historyItem,
onSearchHistoryRecordClick = onSearchHistoryRecordClick,
onSearchHistoryRecordDelete = onSearchHistoryRecordDelete
)
}
}

}

@Composable
private fun SearchHistoryRecordItem(
modifier: Modifier,
historyItem: SearchHistoryRecord,
onSearchHistoryRecordClick: (SearchHistoryRecord) -> Unit,
onSearchHistoryRecordDelete: (SearchHistoryRecord) -> Unit
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable { onSearchHistoryRecordClick(historyItem) }
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(historyItem.query, style = MaterialTheme.typography.labelMedium)

IconButton(onClick = {
onSearchHistoryRecordDelete(historyItem)
}) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = stringResource(R.string.DELETE),
modifier = Modifier.size(20.dp)
)
}
}
}
Loading