From 156809afca51fa05decb7f3a5e521ed5f2f8b013 Mon Sep 17 00:00:00 2001 From: rob729 Date: Fri, 1 Dec 2023 01:15:40 +0530 Subject: [PATCH] Created new search result item composable and improved search experience --- .../rob729/newsfeed/database/NewsDatabase.kt | 5 +- .../newsfeed/initalizers/KoinInitializer.kt | 10 + .../newsfeed/model/api/NetworkArticle.kt | 12 +- .../newsfeed/model/database/ArticleDbData.kt | 3 +- .../newsfeed/model/mapper/ArticleMapper.kt | 9 +- .../model/state/search/SearchState.kt | 2 + .../newsfeed/model/ui/NewsArticleUiData.kt | 3 +- .../com/rob729/newsfeed/network/NewsApi.kt | 3 +- .../newsfeed/network/NewsApiDataSourceImpl.kt | 3 +- .../com/rob729/newsfeed/ui/NewsActivity.kt | 4 + .../newsfeed/ui/components/LoadingShimmer.kt | 38 +--- .../newsfeed/ui/components/LoadingView.kt | 62 +++++- ...mmerListItem.kt => NewsFeedItemShimmer.kt} | 2 +- .../newsfeed/ui/components/NoInternetView.kt | 13 +- .../ui/components/NoSearchResultsFound.kt | 82 ++++++++ .../newsfeed/ui/components/SearchBar.kt | 65 ++++++ .../ui/components/SearchHistoryKeywordPill.kt | 42 ++++ .../ui/components/SearchHistoryList.kt | 22 ++ .../ui/components/SearchResultItem.kt | 194 ++++++++++++++++++ .../ui/components/SearchResultShimmer.kt | 79 +++++++ .../newsfeed/ui/screen/EmptySearchScreen.kt | 63 ++++++ .../rob729/newsfeed/ui/screen/HomeScreen.kt | 5 +- .../rob729/newsfeed/ui/screen/SearchScreen.kt | 32 ++- .../com/rob729/newsfeed/utils/Constants.kt | 6 + .../newsfeed/utils/SearchHistoryHelper.kt | 47 +++++ .../com/rob729/newsfeed/vm/SearchViewModel.kt | 40 +++- app/src/main/res/drawable/no_internet.webp | Bin 25020 -> 16154 bytes app/src/main/res/drawable/no_result_found.xml | 84 ++++++++ app/src/main/res/values/strings.xml | 2 + 29 files changed, 859 insertions(+), 73 deletions(-) rename app/src/main/java/com/rob729/newsfeed/ui/components/{ShimmerListItem.kt => NewsFeedItemShimmer.kt} (98%) create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt create mode 100644 app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt create mode 100644 app/src/main/res/drawable/no_result_found.xml diff --git a/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt b/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt index b04c9c1..ce51ef8 100644 --- a/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt +++ b/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt @@ -1,6 +1,7 @@ package com.rob729.newsfeed.database import android.content.Context +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @@ -8,7 +9,7 @@ import androidx.room.TypeConverters import com.rob729.newsfeed.model.database.NewsSourceDbData @TypeConverters(DataConverter::class) -@Database(entities = [NewsSourceDbData::class], version = 1, exportSchema = false) +@Database(entities = [NewsSourceDbData::class], version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]) abstract class NewsDatabase : RoomDatabase() { abstract fun newsDao(): NewsDao @@ -29,7 +30,7 @@ abstract class NewsDatabase : RoomDatabase() { context.applicationContext, NewsDatabase::class.java, "news_database" - ).build() + ).fallbackToDestructiveMigration().build() INSTANCE = instance return instance diff --git a/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt b/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt index 2d03f70..e458fec 100644 --- a/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt +++ b/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt @@ -1,6 +1,9 @@ package com.rob729.newsfeed.initalizers import android.content.Context +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.preferencesDataStore +import androidx.datastore.preferences.preferencesDataStoreFile import androidx.startup.Initializer import com.pluto.plugins.network.okhttp.addPlutoOkhttpInterceptor import com.rob729.newsfeed.database.NewsDBDataSource @@ -9,6 +12,7 @@ import com.rob729.newsfeed.network.NewsApi import com.rob729.newsfeed.network.NewsApiDataSource import com.rob729.newsfeed.network.NewsApiDataSourceImpl import com.rob729.newsfeed.utils.Constants +import com.rob729.newsfeed.utils.SearchHistoryHelper import com.rob729.newsfeed.vm.HomeViewModel import com.rob729.newsfeed.vm.NewsRepository import com.rob729.newsfeed.vm.SearchViewModel @@ -54,6 +58,12 @@ class KoinInitializer : Initializer { retrofitInstance.create(NewsApi::class.java) } }, + module { + single { + val dataStore = PreferenceDataStoreFactory.create(produceFile = { context.preferencesDataStoreFile(Constants.PREFS_NAME) }) + SearchHistoryHelper(dataStore) + } + }, module { singleOf(::NewsApiDataSourceImpl) { bind() } singleOf(::NewsDBDataSource) diff --git a/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt b/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt index ae596c2..81a9707 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt @@ -3,6 +3,7 @@ package com.rob729.newsfeed.model.api import androidx.annotation.Keep import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.io.Serializable @Keep @JsonClass(generateAdapter = true) @@ -11,5 +12,12 @@ data class NetworkArticle( @Json(name = "url") val url: String, @Json(name = "urlToImage") val imageUrl: String?, @Json(name = "description") val description: String?, - @Json(name = "publishedAt") val publishedAt: String -) \ No newline at end of file + @Json(name = "publishedAt") val publishedAt: String, + @Json(name = "source") val source: ArticleSource? = null +): Serializable + +@Keep +@JsonClass(generateAdapter = true) +data class ArticleSource( + @Json(name = "name") val name: String? = null +): Serializable diff --git a/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt b/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt index cd5e0a4..e4484d9 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt @@ -7,5 +7,6 @@ data class ArticleDbData( @ColumnInfo(name = "url") val url: String, @ColumnInfo(name = "urlToImage") val imageUrl: String?, @ColumnInfo(name = "description") val description: String?, - @ColumnInfo(name = "publishedAt") val publishedAt: String + @ColumnInfo(name = "publishedAt") val publishedAt: String, + @ColumnInfo(name = "source") val source: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt b/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt index 6bf92a3..49ae914 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt @@ -10,7 +10,8 @@ fun mapNetworkArticleToArticleDbData(networkArticle: NetworkArticle): ArticleDbD networkArticle.url, networkArticle.imageUrl, networkArticle.description, - networkArticle.publishedAt + networkArticle.publishedAt, + networkArticle.source?.name ) } @@ -22,7 +23,8 @@ fun mapArticleDbDataToNewsArticleUiData(articleDbData: ArticleDbData): NewsArtic articleDbData.description, articleDbData.imageUrl, articleDbData.url, - articleDbData.publishedAt + articleDbData.publishedAt, + articleDbData.source.orEmpty() ) } @@ -34,6 +36,7 @@ fun mapNetworkArticleToNewsArticleUiData(networkArticle: NetworkArticle): NewsAr networkArticle.description, networkArticle.imageUrl, networkArticle.url, - networkArticle.publishedAt + networkArticle.publishedAt, + networkArticle.source?.name ?: "" ) } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt b/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt index e1937ca..b5b5a86 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt @@ -4,5 +4,7 @@ import com.rob729.newsfeed.model.state.UiStatus data class SearchState( val uiStatus: UiStatus = UiStatus.EmptyScreen, + val editTextInput: String = "", val searchQuery: String = "", + val searchHistoryList: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt b/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt index 4b94de9..c3ff196 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt @@ -5,5 +5,6 @@ data class NewsArticleUiData( val description: String, val imageUrl: String, val url: String, - val publishedAt: String + val publishedAt: String, + val source: String ) diff --git a/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt b/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt index 9178fb8..7638d81 100644 --- a/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt +++ b/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt @@ -21,6 +21,7 @@ interface NewsApi { @Query("apiKey") apiKey: String, @Query("from") startDate: String, @Query("sortBy") sortBy: String, - @Query("language") language: String + @Query("language") language: String, + @Query("pageSize") pageSize: Int, ): Response } diff --git a/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt b/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt index 983f6b3..7e07b10 100644 --- a/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt +++ b/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt @@ -36,7 +36,8 @@ class NewsApiDataSourceImpl( BuildConfig.NEWS_FEED_API_KEY, startDate, Constants.SORT_RESULT_FILTER_PUBLISHED_AT, - Constants.API_RESULT_LANGUAGE + Constants.API_RESULT_LANGUAGE, + Constants.SEARCH_RESPONSE_PAGE_SIZE ) ) } diff --git a/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt b/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt index fb1d0ae..5e5afac 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt @@ -1,6 +1,7 @@ package com.rob729.newsfeed.ui import android.Manifest +import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.os.Build @@ -18,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.content.ContextCompat +import androidx.datastore.preferences.preferencesDataStore import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -28,6 +30,8 @@ import com.rob729.newsfeed.ui.theme.NewsFeedTheme import com.rob729.newsfeed.utils.Constants import com.rob729.newsfeed.utils.NotificationHelper + +private val Context.dataStore by preferencesDataStore(name = Constants.PREFS_NAME) @OptIn(ExperimentalComposeUiApi::class) class NewsActivity : ComponentActivity() { diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt index e2eed8c..87b0ce3 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt @@ -1,43 +1,9 @@ package com.rob729.newsfeed.ui.components -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color @Composable -fun LoadingShimmer() { - - val gradient = listOf( - Color.LightGray.copy(alpha = 0.7f), //darker grey (60% opacity) - Color.LightGray.copy(alpha = 0.3f), //lighter grey (20% opacity) - Color.LightGray.copy(alpha = 0.7f) - ) - - val transition = rememberInfiniteTransition() // animate infinite times - - val translateAnimation = transition.animateFloat( //animate the transition - initialValue = 0f, - targetValue = 1000f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1000, // duration for the animation - easing = FastOutLinearInEasing - ) - ) - ) - val brush = Brush.linearGradient( - colors = gradient, - start = Offset(200f, 200f), - end = Offset( - x = translateAnimation.value, - y = translateAnimation.value - ) - ) - ShimmerListItem(brush = brush) +fun LoadingShimmer(brush: Brush) { + NewsFeedItemShimmer(brush = brush) } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt index ecf1009..9160cd0 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt @@ -1,14 +1,66 @@ package com.rob729.newsfeed.ui.components +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import com.rob729.newsfeed.utils.ScreenType @Composable -fun LoadingView() { - LazyColumn { - repeat(4) { - item { - LoadingShimmer() +fun LoadingView(screenType: ScreenType = ScreenType.HOME) { + val gradient = listOf( + Color.LightGray.copy(alpha = 0.8f), //darker grey (60% opacity) + Color.LightGray.copy(alpha = 0.3f), //lighter grey (20% opacity) + Color.LightGray.copy(alpha = 0.8f) + ) + + val transition = rememberInfiniteTransition(label = "loading_shimmer_transition") // animate infinite times + + val translateAnimation by transition.animateFloat( //animate the transition + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, // duration for the animation + easing = LinearEasing + ) + ), label = "loading_shimmer_translate_anim" + ) + + val brush = Brush.linearGradient( + colors = gradient, + start = Offset(translateAnimation, translateAnimation), + end = Offset( + x = translateAnimation + 200f, + y = translateAnimation + 200f + ) + ) + + LazyColumn(modifier = Modifier.fillMaxSize()) { + when(screenType) { + ScreenType.HOME -> { + repeat(4) { + item { + NewsFeedItemShimmer(brush) + } + } + } + ScreenType.SEARCH -> { + repeat(7) { + item { + SearchResultShimmer(brush) + } + } } } } diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItemShimmer.kt similarity index 98% rename from app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt rename to app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItemShimmer.kt index cb24024..759a024 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItemShimmer.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.unit.dp @Composable -fun ShimmerListItem(brush: Brush) { +fun NewsFeedItemShimmer(brush: Brush) { Card( modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp), diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt index e310ddf..a2c3e72 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.rob729.newsfeed.R @@ -54,7 +56,8 @@ fun NoInternetView(onTryAgainClicked: () -> Unit) { fontSize = 32.sp, color = Color.White, fontWeight = FontWeight.SemiBold, - fontFamily = lexendDecaFontFamily + fontFamily = lexendDecaFontFamily, + textAlign = TextAlign.Center ) Image( @@ -81,4 +84,12 @@ fun NoInternetView(onTryAgainClicked: () -> Unit) { ) } } +} + +@Preview +@Composable +fun NoInternetViewPreview() { + NoInternetView { + + } } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt new file mode 100644 index 0000000..83363ca --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt @@ -0,0 +1,82 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rob729.newsfeed.R +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun NoSearchResultsFound() { + + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.no_result_found), + contentDescription = "no results found", + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth(0.5f) + .padding(top = 36.dp) + .align(Alignment.CenterHorizontally) + .clip(RoundedCornerShape(8.dp)) + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + modifier = Modifier + .wrapContentWidth() + .align(Alignment.CenterHorizontally), + text = context.getString(R.string.no_result_found_title), + fontSize = 24.sp, + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontFamily = lexendDecaFontFamily + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier + .wrapContentWidth() + .align(Alignment.CenterHorizontally) + .padding(top = 4.dp), + text = context.getString(R.string.no_result_found_subtitle), + fontSize = 14.sp, + color = Color.White, + fontWeight = FontWeight.Normal, + fontFamily = lexendDecaFontFamily, + textAlign = TextAlign.Center + ) + } +} + + +@Preview +@Composable +fun NoSearchResultsFoundPreview() { + NoSearchResultsFound() +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt new file mode 100644 index 0000000..64004e4 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt @@ -0,0 +1,65 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun SearchBar(searchQuery: String, updateSearchQuery: (String) -> Unit, clearEditText: () -> Unit, onLeadingIconClick: () -> Unit) { + TextField( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth() + .testTag("search_input_text_field"), + value = searchQuery, + textStyle = TextStyle(fontFamily = lexendDecaFontFamily), + onValueChange = { + updateSearchQuery(it) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "back", + modifier = Modifier.clickable { + onLeadingIconClick() + }) + }, + trailingIcon = { + if (searchQuery.isNotBlank()) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "clear", + modifier = Modifier.clickable { + clearEditText() + }) + } + }, + placeholder = { Text("search here", fontFamily = lexendDecaFontFamily) }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + shape = RoundedCornerShape(12.dp), + colors = TextFieldDefaults.colors( + disabledTextColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt new file mode 100644 index 0000000..d72bcc1 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt @@ -0,0 +1,42 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun SearchHistoryKeywordPill( + searchHistoryQuery: String, + onSearchHistoryPillClick: (String) -> Unit +) { + Box(modifier = Modifier.padding(end = 8.dp, bottom = 8.dp)) { + Surface( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable { onSearchHistoryPillClick(searchHistoryQuery) }, elevation = 4.dp + ) { + Text( + text = searchHistoryQuery, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp), + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + fontFamily = lexendDecaFontFamily + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt new file mode 100644 index 0000000..f95fe78 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt @@ -0,0 +1,22 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SearchHistoryList( + searchHistoryList: List?, + onSearchHistoryPillClick: (String) -> Unit +) { + searchHistoryList?.let { + FlowRow(modifier = Modifier.fillMaxSize()) { + it.forEach { searchQuery -> + SearchHistoryKeywordPill(searchHistoryQuery = searchQuery, onSearchHistoryPillClick) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt new file mode 100644 index 0000000..5102f74 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt @@ -0,0 +1,194 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +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.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Web +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.rob729.newsfeed.model.ui.NewsArticleUiData +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun SearchResultItem( + newsArticleUiData: NewsArticleUiData, + modifier: Modifier = Modifier, + onItemClick: () -> Unit +) { + + val context = LocalContext.current + + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1.0f, + animationSpec = tween(durationMillis = 150), label = "" + ) + + Surface( + modifier = modifier + .padding(8.dp) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onItemClick + ) + .scale(scale), + elevation = 4.dp, + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row { + Text( + text = newsArticleUiData.title, + maxLines = 2, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .padding(end = 6.dp) + .fillMaxWidth(0.65f), + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + fontFamily = lexendDecaFontFamily + ) + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(newsArticleUiData.imageUrl) + .crossfade(true) + .crossfade(200) + .networkCachePolicy(CachePolicy.ENABLED) + .memoryCachePolicy(CachePolicy.ENABLED) + .diskCachePolicy(CachePolicy.DISABLED) + .build(), + contentDescription = null, + alignment = Alignment.CenterEnd, + contentScale = ContentScale.Crop, + modifier = Modifier + .aspectRatio(1.75f) + .clip(RoundedCornerShape(8.dp)), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) { + val (sourceIcon, sourceText, publishedTimeIcon, publishedTimeText, shareIcon) = createRefs() + + Icon( + imageVector = Icons.Default.Web, contentDescription = "source", + Modifier + .size(14.dp) + .padding(end = 4.dp) + .constrainAs(sourceIcon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Text( + text = newsArticleUiData.source, + fontWeight = FontWeight.Light, + fontSize = 12.sp, + textAlign = TextAlign.End, + modifier = Modifier + .padding(end = 12.dp) + .constrainAs(sourceText) { + start.linkTo(sourceIcon.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Icon( + imageVector = Icons.Default.Schedule, contentDescription = "time", + Modifier + .size(14.dp) + .constrainAs(publishedTimeIcon) { + end.linkTo(publishedTimeText.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Text( + text = getHowOldIsArticle(newsArticleUiData.publishedAt), + fontWeight = FontWeight.Light, + fontSize = 12.sp, + textAlign = TextAlign.End, + modifier = Modifier + .padding(end = 12.dp) + .constrainAs(publishedTimeText) { + end.linkTo(shareIcon.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "share", + modifier + .size(14.dp) + .clickable { shareArticle(context, newsArticleUiData.url) } + .constrainAs(shareIcon) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + } + } + } +} + +@Preview +@Composable +fun PreviewSearchResultItem() { + SearchResultItem( + NewsArticleUiData( + "News title", + "News Description", + "", + "", + "6 Sept 2023", + "news source" + ) + ) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt new file mode 100644 index 0000000..024939d --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt @@ -0,0 +1,79 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.dp + +@Composable +fun SearchResultShimmer(brush: Brush) { + Surface( + Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp), + elevation = 4.dp, + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row { + Column { + Spacer( + modifier = Modifier + .fillMaxWidth(.65f) + .height(15.dp) + .clip(RoundedCornerShape(8.dp)) + .background(brush) + ) + Spacer(modifier = Modifier.height(4.dp)) + Spacer( + modifier = Modifier + .fillMaxWidth(.65f) + .height(15.dp) + .clip(RoundedCornerShape(10.dp)) + .background(brush) + ) + } + Spacer(modifier = Modifier.width(6.dp)) + Spacer( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .aspectRatio(1.75f) + .background(brush) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row { + Spacer( + modifier = Modifier + .height(10.dp) + .fillMaxWidth(0.3f) + .clip(RoundedCornerShape(8.dp)) + .background(brush) + ) + Spacer( + modifier = Modifier + .height(10.dp) + .weight(1f) + ) + Spacer( + modifier = Modifier + .height(10.dp) + .fillMaxWidth(0.2f) + .clip(RoundedCornerShape(8.dp)) + .background(brush) + ) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt new file mode 100644 index 0000000..d185feb --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt @@ -0,0 +1,63 @@ +package com.rob729.newsfeed.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rob729.newsfeed.R +import com.rob729.newsfeed.ui.components.SearchHistoryList +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun EmptySearchScreen(searchHistoryList: List?, onSearchHistoryItemClick: (String) -> Unit, onClearSearchHistoryClick: () -> Unit) { + Surface(modifier = Modifier + .fillMaxWidth() + .fillMaxHeight()) { + + if (searchHistoryList.isNullOrEmpty().not()) { + Column(modifier = Modifier.padding(start = 12.dp, top = 12.dp)) { + Row { + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = "Recent searches", + color = MaterialTheme.colors.onSurface, + fontSize = 20.sp, + fontWeight = FontWeight.ExtraBold, + fontFamily = lexendDecaFontFamily, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 12.dp) + .clickable { + onClearSearchHistoryClick() + }, + text = "clear", + color = MaterialTheme.colors.secondary, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + fontFamily = lexendDecaFontFamily + ) + } + Spacer(modifier = Modifier.height(16.dp)) + SearchHistoryList(searchHistoryList, onSearchHistoryItemClick) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt index 28bf92d..679a3cb 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -170,8 +171,8 @@ fun HomeScreen( @Composable private fun LazyListState.isScrollingUp(): Boolean { - var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) } - var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) } + var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } return remember(this) { derivedStateOf { if (previousIndex != firstVisibleItemIndex) { diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt index 19045a6..bf34f59 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -39,14 +38,15 @@ import com.rob729.newsfeed.R import com.rob729.newsfeed.model.state.UiStatus import com.rob729.newsfeed.model.state.search.SearchSideEffects import com.rob729.newsfeed.ui.components.LoadingView -import com.rob729.newsfeed.ui.components.NewsFeedItem +import com.rob729.newsfeed.ui.components.NoSearchResultsFound +import com.rob729.newsfeed.ui.components.SearchResultItem import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily +import com.rob729.newsfeed.utils.ScreenType import com.rob729.newsfeed.vm.SearchViewModel import org.koin.androidx.compose.koinViewModel import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchScreen( navController: NavHostController, @@ -55,7 +55,6 @@ fun SearchScreen( ) { val searchState = viewModel.collectAsState().value - val listState = rememberLazyListState() var active by rememberSaveable { mutableStateOf(false) } @@ -63,7 +62,7 @@ fun SearchScreen( viewModel.collectSideEffect { when (it) { is SearchSideEffects.SearchQueryChanged -> { - + viewModel.addSearchQueryToHistoryList(it.query) } is SearchSideEffects.SearchResultClicked -> { @@ -76,6 +75,7 @@ fun SearchScreen( modifier = Modifier .fillMaxSize() .background(Color.Black) + .testTag("search_screen_box") ) { Column { @@ -130,20 +130,30 @@ fun SearchScreen( } UiStatus.Loading -> { - LoadingView() + LoadingView(ScreenType.SEARCH) } is UiStatus.Success -> { - LazyColumn(Modifier.testTag("search_result_news_list"), listState) { - items(searchState.uiStatus.news) { item -> - NewsFeedItem(newsArticleUiData = item) { - viewModel.newsFeedItemClicked(item) + if (searchState.uiStatus.news.isEmpty()) { + NoSearchResultsFound() + } else { + LazyColumn(Modifier.testTag("search_result_news_list"), listState) { + items(searchState.uiStatus.news) { item -> + SearchResultItem(newsArticleUiData = item) { + viewModel.newsFeedItemClicked(item) + } } } } } - else -> {} + is UiStatus.EmptyScreen -> { + EmptySearchScreen( + searchHistoryList = searchState.searchHistoryList, + viewModel::searchHistoryItemClicked, + viewModel::clearSearchHistory + ) + } } } } diff --git a/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt b/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt index e7da52f..2960990 100644 --- a/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt +++ b/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt @@ -42,5 +42,11 @@ object Constants { ) const val ERROR_MESSAGE_PREFIX = "Something went wrong" const val API_RESULT_LANGUAGE = "en" + const val SEARCH_RESPONSE_PAGE_SIZE = 15 const val SORT_RESULT_FILTER_PUBLISHED_AT = "publishedAt" + const val PREFS_NAME = "prefs_name" +} + +enum class ScreenType { + HOME, SEARCH } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt b/app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt new file mode 100644 index 0000000..31147c4 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt @@ -0,0 +1,47 @@ +package com.rob729.newsfeed.utils + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringSetPreferencesKey +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException + + +class SearchHistoryHelper(private val dataStore: DataStore) { + + private val SEARCH_HISTORY_LIST = stringSetPreferencesKey("search_history_list") + + val searchHistoryFlow = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + }.map { + it[SEARCH_HISTORY_LIST] + } + + suspend fun addSearchQueryToHistoryList(searchQuery: String) { + if (searchQuery.isEmpty()) { + return + } + dataStore.edit { pref -> + pref[SEARCH_HISTORY_LIST]?.filterNot { searchQuery.contains(it) }?.toMutableList()?.also { + it.add(searchQuery) + pref[SEARCH_HISTORY_LIST] = it.toSet() + } ?: run { + pref[SEARCH_HISTORY_LIST] = setOf(searchQuery) + } + } + } + + suspend fun clearSearchHistory() { + dataStore.edit { pref -> + pref[SEARCH_HISTORY_LIST] = setOf() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt b/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt index b18916e..57cc9f7 100644 --- a/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt +++ b/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt @@ -9,6 +9,8 @@ import com.rob729.newsfeed.model.state.UiStatus import com.rob729.newsfeed.model.state.search.SearchSideEffects import com.rob729.newsfeed.model.state.search.SearchState import com.rob729.newsfeed.model.ui.NewsArticleUiData +import com.rob729.newsfeed.utils.SearchHistoryHelper +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -21,8 +23,10 @@ import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container +@OptIn(FlowPreview::class) class SearchViewModel( private val newsRepository: NewsRepository, + private val searchHistoryHelper: SearchHistoryHelper ) : ViewModel(), ContainerHost { override val container: Container = container( @@ -31,22 +35,30 @@ class SearchViewModel( init { viewModelScope.launch { - container.stateFlow.debounce(1000).distinctUntilChangedBy { it.searchQuery } - .collectLatest { - searchNewsResultsForQuery(it.searchQuery) + container.stateFlow.debounce(1000).distinctUntilChangedBy { it.editTextInput }.collectLatest { + searchNewsResultsForQuery(it.editTextInput) + } + } + + viewModelScope.launch { + searchHistoryHelper.searchHistoryFlow.collectLatest { searchHistorySet -> + intent { + reduce { state.copy(searchHistoryList = searchHistorySet?.toList().orEmpty()) } } + } } } fun updateSearchQuery(query: String) = intent { - reduce { state.copy(searchQuery = query) } + reduce { state.copy(editTextInput = query) } } private fun searchNewsResultsForQuery(query: String) = intent { reduce { - state.copy(searchQuery = query) + state.copy(searchQuery = query, editTextInput = query) } + postSideEffect(SearchSideEffects.SearchQueryChanged(query)) if (query.isBlank()) { reduce { state.copy(uiStatus = UiStatus.EmptyScreen) } } else { @@ -60,6 +72,22 @@ class SearchViewModel( postSideEffect(SearchSideEffects.SearchResultClicked(item.url)) } + fun searchHistoryItemClicked(searchHistoryItemText: String) = searchNewsResultsForQuery(searchHistoryItemText) + + fun addSearchQueryToHistoryList(query: String) { + viewModelScope.launch { + searchHistoryHelper.addSearchQueryToHistoryList(query.trim()) + } + } + + fun clearSearchHistory() { + viewModelScope.launch { + searchHistoryHelper.clearSearchHistory() + } + } + + fun clearEditTextInput() = searchNewsResultsForQuery("") + private suspend fun SimpleSyntax.updateStateFromNewsResource( newsResource: NewsResource ) { @@ -80,7 +108,7 @@ class SearchViewModel( (newsResource.data as? NetworkNews)?.let { reduce { state.copy( - uiStatus = UiStatus.Success(it.networkArticles.mapNotNull(::mapNetworkArticleToNewsArticleUiData)) + uiStatus = UiStatus.Success(it.networkArticles.distinctBy { it.imageUrl }.mapNotNull(::mapNetworkArticleToNewsArticleUiData)) ) } } diff --git a/app/src/main/res/drawable/no_internet.webp b/app/src/main/res/drawable/no_internet.webp index 087948055692d4b7af7171b208fe98d9772a0a64..949fd813ba97dac975a4030b2a41568f0b731204 100644 GIT binary patch literal 16154 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 Yy`lXC&-oqqd2kO#T)D<|{`~6y0l>Z|O8@`> literal 25020 zcmZs?1#le85-m7lmc`7>%*@QTn3|yM$LdG=Ca0HTUXA1u@Z?Pl>#zlR?N%&+W>wK5iW)aNg>c{v? z&=fc(of?;8HJBGMX{@t8VNj=Mm2B+GFHbf09g%YHSk6II_loa*k(_cb1F71S{XkP8 z=#O{OUQBt$ZsBwdFTE=S;Cmx$ADR3gjS3i-d0nUdOqrY9E2ZQSMRr^+fz}dU8Y93 zhX^h*J0I3~3_0>OgZ;b)C|{4aL-VHn>EA~DL!G7hzdQar#GTUchq==Iq#1j|x8L5=N%)3TV`<&-_ZkSCycLfulLToCr0s(OFC(TzNf>9Sv8?V;mHm&_DP=_3 z9&nbz{S#s$f?jAJ`ohuN=RciR#G`pUushchI3Rh%j+lUuIJ&EUZvOWKUW;A_$B1-XK^{gm(1HE%(v z%ZD!#sFdD3yo7y*zrsCCt8Tu5$K&v#^;zt8t##yGxWisaF=kk}hF&-=355&UZak$# z5EPyUR_)Kez3Q|g{pZBa?QjB9N-dAGw=sAQIq&g672#-Z&-ch(-Hz@QZD}?1r=wsM$U#dAyo-UpwNT0!dUt?jF7uG@pMXgH z$u}Py^lh2y=DSVV0}9XYqQ6EumV={;RdkQS&bFd_HjPam;B9aEmhXEA8&vb5mPc8( zSU|RRm9qogmc<4&Ogq(zq<4sxD&_rD_Pl>;4@!v{4D0kb=8}`ZE<(L6F#$7I0}#t= z?i42{k>Id){y`ktijR_A+jR|HU#{eO|GC-Me_AxMY&OL{Pjf=y-%kb2UiXhu!DD|{ z#+1vI4}`Ip}?7HK&M5@d6eHKW)#VL{6!{B+2``MMD(#6UrKY9r5|er2FEm1 z<6n4@1_Uf92OJ8_;Y6CpK@LB9(6c}w^}046t6{c&-?vtH0LOF^CHchGzYtR;m2X^K zVl{>p>aAlM1i?>;oT~`AW4*^v`0GAgEPdZAL8i11LR8l&_9-R1h2j~#2E#o)hzq9` zv`B{#lwqfQ5fvC%)3i}zj~x63B(ggy^4vOhyTjFS56dHeM(LTELL*mhzn#RdNttLH z9<&<7w1}?R;Q3#Ih4`$eB-zS7+>0r8$}Hz5nPnx}RQ1@AXFAGHy}+}4kN={1n`_lD zCW(8u)wH$v@UsI)dz^_@rIdXpL}4AWNOMOQIG(yuPNmhfBpOzsIY50>($CXvW84k5 z(YsUbZCMn?B#yM-BgAfB=>wbG{}1$<`}!yy?{!xfKjBH-C!w+EH{oa!e{EXGMitf| zJNOj|+!c^(@8>Y9?$YjydAu?F^T(yC|6Q$Eef1qIwS{U#T*JLTcv)vdB&max&4UUy z`AqP;Nb;(T6rSD@7&RUkIiQLI5OpfGlG5-$=}0I58;TSsNHGk3gP3SEdsLFDw!=D) ze+k?tx%|U1eb3*sOFFPW;r)Y&5aTry46e6%`j?sUIb-gNRs;+<`S?I>t7@JgYc&g1 zI6@`@5U**6{qH9JC8AGfkBGB8-zY)$1wz{P*}U829babm)K9aRI^GMv;S5A1h{k}5 zh)d^GF&;#RDRw|R>DiUA6cA)OO|Be9)^3bmn)o~ z!{#?vb;cIYhbdh0q%4+1D<|Gt-Y4GutR`;3^|HQGHbZ!VE$%%Gj@2@iHoJYxN?Sv> zKwUc@pnYUVQL1e|(pL!RKE3E1=?yXk8^@vzqNoIZ@Ui%Hkcz9kn9Iqo!VW0j;<5CG zcIV>3sbY*;>E~LJ|3mFW68jzQh>{}|@~fZxoQ+VDSD!MCth;^ZL8YnV3$61b$d_Wr z;@RS+zae6fx$#iI29gO;=w=o8Tl&|`^h6PkzvhbQeuOe)m0~l=CuRBh)@emJY)Y2R z$FP|c?Yn9<^w#M!^QdQ`smc!qJ;$qZzzG8w>(-dt4R;+Y*)Yl&-s{A-h={7hgvCExlZ z!Na=Ul-ktK3vJ+rzYi$cwX&;0mSXLa`%p9#w5bLZY}u~XiZy@kuW@_#bDx9*T2`fQ zC(h4MI9!CxKLy0j4`vfHnI45o!|2C;sP+i;`g`eXQMYAM5Z-UeI(ZGfzd@mS03u$T zFHpbby>#4j+)eDu7VD4@ z9YY5WpJ-+LxH#{64nj{{n1ufX&gB{B(m`%zI79i8`>9Fj-24pD2gCwcj#}fA$RHWF zW8-kBB2tX#t7Qs!2_@fx2A6Adb9uKHbd zyKjxN)PzZ7^SWpGLHmv zl@)%Me3gE{MkM0UR8_f4(<#;&INx^}AQ!p6I`pK6wErPUXSHlf9CkbqIALa&296)r zbZeH%{pU*m<4OFt@>QYX6EipTFqJW`kNu~T0tlK`tKZUnWXa#@NeRk6#xIS! zp&yVGewEalQoe6!wL)a=T%!m5`BMgA@~hKqyg~3OziCDp*p#d&6ZnPvrpy`mzKe7B zKlJ}+6F6F^^zEi%{#owjeMajV(ixW#{>6v2J2uur)tQ|ti{>kDnmCTmB6)oI3DY2Hr(w(W9kXU*9y<0GF(Z9gg| zXGT|OTgbe`_o0E@bSLdO>4%6 zn)_F1kYK;ys}19Sl76aeBOkLhr09(P1aD+v;NTnGNm2{wLM&aXtvN zyVWSBOx9G6!8Lq?b(Li$)GX()hi6aj{+xr-HCs9YYkaSiqwD`l&cAXLT?3QBs0sSj z1hy3rSk!*2i3C)yUQIHsDm|kNk5R;)@1@hSHs>USy`@R(pt%!%;=4>k7wzS@3pDT< zY2pI?a|-d!PzFKVbVS?BL68+g<$uMqa5TXHw&9UO8w9E3cXX%5%hEfuw~y%qRJuw+2I9` z3d8@(oKO9J`|@a-$SFKmE+YF~n!ECO(_DYXd|T!BLxztWr_^tUpC-%r701UtI<_TE zIt{g!pht7zy;pjgOGwcHYR2t9i1UY(CtW5G3pdN9J5TMv!Fx;=iwAl?iSZw3@KI`> z+S5vMGDIc+Sa>EWC6@kEExzfL6FUZJ!Y)cQH&~Rj-=no?J+XdB#E!%v?R|o-nv1o# z`T=|WKR^AervG}4cOjM37}fIaWP1`wREb}Y1emMgU|Rin~d@QWFNx#odc2U&!PWN+Ekjbx%L{L zA1^}pLN`Jt?2KMmZ0oCL>afPI??5;bAh13PSB4lf&=OyoipNPY#P4aJ9i4lh^uBPV zbj9U8A^Sw}cH;2>75<=8UcnUle>-6%x9=Y|?-y7hZ7f=LE=3&;y_qNkFrobVMKNV^ zt%&8Q&^GdVx_k0`dC_X(4Ka1|vNzY3Y`@<@#bnQ#h{Da!tlxz*dI5V~mbX?N{QY~= z(s9?1+8;HU3qNL{5>*r4QyvM;rT$5g{<{^^if|^pJlKL_>w7pt>NvVE1s+O|UP@En z^t;3=t31}0)wwWvLeCu#ipXF(97ka0M3`<^5hl&o+A`QZ_Z4kjjC|s~*7LOnaMi zvXYd~L+s=L|MKf5EfBZE-1AkR?s$_n^OU7ef7LxxFXB(Aq zTV!ld6a+AsC*f9;jvHi#rua$`_kg_(l?*=Es>a9g@?f1vzB>26#BHcyLc;x~odWuI zz1*c#eOlGmiQ#J)WYfe-^dI6}!k)oHiyY`zN+r${5vz2X<4FkS-_!HHQCP>=+zQIb z?y=LadJxM5q_*0L79(P976ZZ)`d@UDAny~pyQh2FRras@^6P@5SW{;e}# z07$Cl;!|I_(|sUtCYpt+#cOMQ#@=?jp@uIw1%`T2OTXB@Q)p)rW@JK?{v&p$<( z5|^>U*gp2Z47KR$u&>9v&^mW>$W!==qaCq;KSiUSW3B`zWbj<+@^8rWpAr466yUgl zn*HO}f3sj(9M!fdI~?eM{O$mXQ?jdQSK|+B-y32?RDCFnsz19=)61uQ#QVR6ga21a0P%mVxfR(^b4?pm z@u0r$p2dTvyIKaDVDpnDbuSx!b}S{}wrlB3g3UWl)Rj~L;NxvB85sRFcum!_Y@96B zERQ-!rdbhfkX*Ao${?v`ag<3~&2m43teWjc4n;M~qYQ>-hVLu*|KE+X&WyIMCrwQt z!7$u-DYTV-@?fWsO$xG_nt;KOoH!ZurGARQ2QW=a@|v0egCPY;a@bS-WTBV9n&hMv z4dTO=5iJkTkpJ(um*^6>+#9C@8SK49pEUc%*sOlOecTykKaAFFuhDVw{66oa`l9SHZ6QA4KFOM0b zZhgubVRRLvlysR6U)q$I!OsOx8P2uXFNndSDuP)1s>j15a3jV6yLy4+ugo~ZPY4H^=27ZxENL?}`GI+4EN$FPUkU6?yJ>5Rg^=Vj$UCZN810}u^n{9Y z*dk9x;QrY1_U>u~5*^vH)i;LRS2v5vb5lX1dTA$n_p9ypGoA|h9p@pGDju?Lkd`X8 zY~NXBelhI+3Q~Y)qGk6u45it~ydW>Q1FRYSX1Fm}xO-m-{wBa^0V3W4m)Dh4=QTYB4B1$^Yb#&tpa<3^bwxxjmF zH8F{;+Ors@Tg!%Y%%xfy6vVpB$UD<#fgR9_W|O|dcJx|^n-3O*<;y|Zh1_|~MI63p z#9N4)N|8jBx$nY}nT3G*v5NWkkrs|>ShXvr>5AYUSM2R_z~P(^A5r8~XNtLJL%Zb?BqYvtH*D}Sb2 z;a|(+(W)Y)e*1k*xJ$1JqSQfTY=*7HEgZLHTS!1bcf+2DkdonoGvP~ex?A>y zZWDvgr+FzvZTVY4snQ-*_dSo3v^}=MR_9DsVNSl#sl<4%t%XIWAuXBgkrXOdo9Ht8 zO8nk$(s+Ct^HKY>rS;z2KyKG@?m}-rk(8v^liH|1fc7)hm+1Tg_s5Ipx={nd@JAIW3ze2a005*|EC8Zu+j{-s z1C>MgjW>j-taCmg;G5j`BERaMNw16;%e!3U;S>uRvym&Ic~txYn*AvL$!o8*bb*fm z-%X>BX%daZE)Nw?>Z($gFVJZyI5_V1Bbb(CBZR)oCGDktxR9Z*Mk!7-N^f3>IZpO3iQq;zl;yn2RvA(A#7R~tp)WcSO-fLDd8Xw1M;x)czRDTCYk7gHj}lJ)7! z=MRt|oF~VzU$cugXNN+#)^OE|U1L^nT*aBlU-l6yrkGKZ$4=o|PDEF<#LbDrgb;<= z>hM=7XL2{fq#N@tD$DOc_qbLkuPwMH)NNM0ZGf$&Oqgs=9ha!Ec@Wh83Hu&M{iQ63 zy`G*6000rZ!_?-it80pI!tCnDYsAv5m?$f8>=yKFA92)oj2(JuP9{fFIcq070a3Cp z9txrsn@;||3{#!6-I3#*F?n?z&SWG`v?_fEXPEnUykk1YVc9xH6C-2FQ-cEcbelFH z>K*M>)c}Q-6u3+ekSFaLXXlbv`5L2EhYGUITDnEf88ucQGOa3DJ1LMn9jp}nXQNnW zF$Xh&9(6Qa`{|ID0C2*3*Pg!O0dr8lCvJ)6lU@bJ95C^d4 z1polxjdYoBw;6A=t(oQb0qJHG?BopvM{=S!hOiCRi1rXy9R(s%u1~0yR%KY2%f?^N zR9%Y{+zQUWE$xh{GPXhw3ZD{_4;h$MFW10)dhhQ61Gl_rJtThpLE?d@4~dcnhi6%? z1XgIf1>8hkLf+|MIm~E%Wo(Zd`5F=6@;ue2J$C@(Nx9<;W{w7eHqGP#+&8RT1_# zMi&^hVaO!<nPf^G+^cH%jGV^$F7vvibF}HX z%G>op87hJc?5EcAEvAGhU$bgrDR|C7`t?*B?`WMjK36N6&Em#nP@Y5XpK=Q79mFfU z{Y1a`$ICot!#rf!ULJ0rou-SR;qDlvmLhX=NEjoIZl*&7ntGyjln{7qR6qhND0^IH z@dB_WXv>D3Q6)MC4V)h1zHVZSEf*w_-zIZ1^TJCJN%P!OW}Ft$9Urdedp2%3U9$qF4VeB%nE0LiNR? zILi+p>U9w*PHI!qvn&p-y>TcorFr}`9kqSWtP!)}9)Cw#5%hEczdaQZj_{231^|>& zz^Jp`bZ1wg4=x!|6onZE$m5TCKXYdO#QA0Oq`s&7En-<=Z9YNP^rFO`2W$ikSD2S@ zYrCJz=TdTLB&2x?lE%3QhTFeUPgBSFk-QXE{bgDlh@7~xD8wJI-|3ivYM*^BrR0hB zimO8mM9}kq)|U0tgiYMRt|(f@=btrTxfS+a%`Yr>o{Cd*$$akLtwnX%%$xu5GvUd3 zhyvo8`VHd5U8(c!OM=zj>9gMcX)XQhjxb*E-4DUD2C7s0KDpPIG2!NEEuSzxW?Rfu zN}*EQT}`fnpf;cFlbH=`23oK2=>5_T*V_tF>R&J2ixM|!+^^P?rFl{mtFP8$H<`Lw%SLgCSrnzA;GEl0pw1iEWv@TKHtGTd%ysveqdh~Z=U1Fbvuz3e1!(?V+%I}xyj{k^#f@_g)EFQa8Per4t@u+k84`sCe3OV4=p6e7a@d` zdX(17TdX%qc)7#7Ax=5hvt5cfVp*U}wnbS?*}m-R z7cz9YZOL;MC!^b5M#8#(&lOdIX44&68j{Ae>QI`q^a|`9l;4mw0bo+!(KcnA7@B=J z-U_=5D`w(hDhU*TMEQ1EHrk;F5C~GcKjS6Be$D(nXIjT-z(Txe{Oh?!i^3GNq5A_+ zf^!9o(Y#p!3A$aQif13_oxWrIc}J8b-#yNW>NJl^fgUEV<-7FID$nNnI2r?~^(@@c zhEs+C)&FblW`smd#dXUsXAVKkKEenE#b^cBN0@!O|FV;a-zCJPT)eE^gl&|r`M5pg z3X6u4?HK%A%&xSw(5zgOJJh|`CN1SWp1gI4wlquNz|a91%#=0=qTIY%y>xuXC6b7? zG}?7JU0XI(#u{fg#vN?C0OcX_7`ZQ6)$Ent-baN}xShWuUa@bEgaYz_3h_(-u3K$s zFrP#I^_b-cL!(N=wpNGWpPk86VcZE^BHC;0nd$a?W&vA}lLPHEcZotLAiH_i`ctEL6{cZ)9eeD&q?n zR|W3y#M`*zor;u0?W#b|bD<9h9H90+3BqbSCV7Gb0CF-NBP8bxPqDI`!#bD%m5{3& z+KXwOJC}ip|K`-ihwoHh*N0UVj#YVXzmI{?Fc<1HJ!Km1V<`mIjkdDCc!Rvt3KRB0 z%5q--V51`u(f=cGf8FarGilSt4F3*7n*`*gP~`dv6Lv_hOP~`N&6)0f8;YaX*Yby% zJQTr!^Zq)BhCRC*w<*oY=&&eWIcs?NM#?W$ZD{LS#PjFcQ$>Fu3&DlJyCtD^ZMaz} z19v%TR4xh4Yu8!xt_aFZj;Eeoy@BUy4NYTz&SY0Q)JtttpL0H<@n8<(k~}tuQVhT~ z*=CwuGnpn>;T(@7YyH*Yg2~Cbe=P?U<^_?9en03LGb)6n6^|T>Ag#+%!iNc2VsKq9 zk_;7}HO1n(dEu^=E0EUWbjl%HeZr&;N@?_!c_P~RxOHujMX4gNp&_%hvB8z*loFs9 zOp&7MFCEK7)a=0|{g4-Z@`@l6&vs`MHX<`X;fkSGlP5KD_qEJ?nAQ}v1@&Hvvi0)A zZ_9=W#+@KLe|1!ofvvpqVxGlF{jk&9o~r7xurUW9=*MDknwx%&Do0nK@oi2Y-AvCS zwK@TKF%%LowxWtYzg?Gij|r2jeVGzNF@+ZL9|@vU0CLgia}$A>Rp*FT$cDxxai^5y z_lMal3KAo=X*u*-&2u^&ubyvM2;wJHJ$0ARMbI-WcRxE@1T#K-8Bh*=wpE2;)Z9r> zX&VtIG_5RO-@Egky6*xODpbRC#@^P`U$$(AE?K(73ZhIvO4@qT8t@hT%6*SC zFRi<+3PSHy-BK{#1Y0ab7KU3V(%b2F)HdDUso`+BUrp($JWByml8}AjpHM==zY#jz zJ7j}u=Hcpb+poo33`~Tgd9Vy4C@UM6HLow?kC?|HIG3<; zDpXpuP!Fv_UVGZecSQ2A?xhli##FMH`GlqEt;iHk96r{|vt@v+Urv(3T1O}h3gUZO zH57gYE{os)?O^sh2w^tmKc0~DV@;fB(1Pfj+WLmQ#SRyaSEz>b!+QAV+8cCQzmn!A z=kShy+e%doAMF$5J|gM{o;>5S;CaAcv5+C5eb}Df9iyGz&)!aNr+Wt~RCI7^A;UYf zeHbL$fUpxf3)kNP6)@Nd%7ZVY@V*#T#XOJSkY_+ng#6M;*D2pdVV^3e#<1w%;^yZ# z1P^>5kiO*JtI6o=0UMj?2Sy{xeESC?n^3RWXN0+{+FNreVt2=m-JJ+_}Tst)c%6qsgwyuzG-Y z%>z65{_7)T(FnvJsRmrp9=GzU@a;ApGSA0_tA8)Ps5SH}?aZ~*W4%j+rr`V8$c;4* zZP0GMd5)5gJo!PJCD@f573YS0mZlP@(Mu%wQ~%^!@*sw{Np5DVTFqgu>Msrkm+{eT zygCG>N#+jPA3k!s?0&1u>YQe6yc1kv2HF={4p9^9<=@Z2u>947wUJp%o-RCs6Z{@-gB;gzgV`N?S)jw83OII{QZ<613@iTc5;&zy5; z{O{HrX%hTG*1^!rb|gK_PmuT9q}*mMROxr5A7{1)hU2M!wp;0|g+1Q@ZCT$`MEMlJ-`a zS*%ptqoix|{wxp=i@c7X;65Vu`=!qxu>fpKfTZ}|t_Kt^WAOLx-=rV3eEc3QDP;oL z9FgWPlIn9+CAbS+R5sYn&J^=ttj_y_@2l+=yw2!5K4)qZOL-6 z82uS#G~Ta#XAkje(%xM;BQOJYrI$3<(Tb!nQFRC-&Po(QdXZyCg*L8}I{f8P7fHl# zuO!SkNEKqLsWk!2#xe;$rn}Y)%&D|b{SG3Rx!^s7s9?QRd{6 zwk7TqqVcAVu0$|X|L_>QZ9jqr4KOw+MCltQWz6PFL5P2q%Dx_?`*duQm$ zZ6qx}zHTelD~_;3A5L?h;_K!iD}~JAhG_V$+&$*NzglACw&1c4xrsJcOXRQP`0#is zBtU!R>k&sS-C^XyOsqVi$+2oTBLXcT4Z6wq=6w00_nZ9gdQU330$M*jc4V?-#OxiH z7~&CvZ!_=+MAs}Ka=WD`>19sodCWLW7n90gQYu`mD1Nu0#k=RHXO`Z{2_ zs3+zpltKhzp>I=1h2zBW;!vQoEm`^?{B~P)c*H$A007Zdj*a#Xu03Qr8~Fv)3_5Fv zFKv!~cfOvir&5DQJSc}VHCBYZS%G%F^oRj*i%b? zOeRVTUHg0T)=GS5i-}d>nOeGMW;C3uSB>$;MzsV@yz~^zF5U`EuAI9-@mn^=CB1Eu0B#CC5fmcp$r7P;) zvxEQkL~LLXPco(14W>5mX$)yP9^Eqyv*jdfz}Jq~ilT&9P$WH*r3h|@Un5u;{DoW* zRiYZL2Uv;^(i>NPsbSUnk`+ zuPMaVxsqbO{)tRIQ86-VaR-6sTxJMEqVDKt-=W@>6jL-o6$Q3JtXUE zG9T717vp%0W%Ev{_}|hN4^Kbfuwos1Dn!Wx1p|T_FpkYD!zgpLYUY91XqbKxWP0*@ z>G3dsY8vD<S>dP1v_QUF712Z>=&O?EX>xE&4Un>YvZ#A$@;RGjLJtHk;e)CUHZ;LQ9E#6_KL z0;Hb9n%q3HTHyc2yCue;jd}&*pD`h5Y_*p*wBjV?lO!c($HF4%@?-=yB*gIlL;njuzRw~Yg_@U8D!cXLkpSKU;=+l@8id)mHJaaS!ts-+7uNEvE-6Rv? zek{&41pLM%P>ua+C&F1jv~MRFH*CRG>9rC|GPdi9-%8=;i3=IVOY9-p?o9sWg7swOK2xK;qB$(qs_& z1;NsxUp{Lp?^STbW4wiHv3+*=#OS{Q>zwFp_RRLqbM^uu!y_n{U9u?iBx}0P?tYB% zTn5JS&Bzg5N?~orF7#eU5bcMNu>J8?JA`!b>=VXQHY{|Lwg?$2zw5zEzgD0ZbDzGP z@`W*V97+!MJ5MQ8TqTlN@vy^S&a;QAPP7pn)&y70^AU56hZ`5t;IT6%(1Hu!sX+SB z$^o$FawB)rkN7Njp0J8r35jbWd`&nS{XOsHtrc>+Zz7@N?jcYV=I;~Oh+0adIQ27E z2+aNjWfuQxfp&!nPc8|9;=8wivs+T#yi)<&2)>;nSm)V~h_)m2qkM*un0~H}mlBQY zUHbAkS8AhZNhdvZ6PRN1;~>giFMjEBSBcfd!u4aWRWv!~RW|LS5QQ=?6Ns-M6EzH# zf1cVIXt$CC2^gwJ5gRhwJ={RnvEZc&nOc%_;8VMCmzbK5u)i~68Kths_ggIqBB!cX z*j%IdTO%YMFcR*1uWKby*X4HhyVqTDBZU?K(7sBThZkem+fK0afj`hId)@WjzV6=k zD{-{YWg7hSybr+6yUe#-Dumw7zKVBNz7(+`uc@^v{sjpY&iai%x^8n5i?escB}2@D z0PcDnkf;lnjWwto=i5mF2Cv+X-$xkHDDgAJwR8vHTLum^U<0pKj$O{zF6_cM1Kd6+ zVek4tRVqrJ9#NggpEW=vie8Ke)wuPZ7dy4)J_!Iq#9&+;TPdS!%#xKH@MY8yuR!%} zG9Nbajd_t%d50%(WfE5NVr*?J9eAaFu>5*+ba2cP1ha1HBPDZc~ z9NT<9)2$J>#crX!8qnsfMr2(=jw|o}q%OPO|C=PtoSfg~F zD51K!)+*m-!}tU%4U%}ZYRY=I^n-pXBQ zmFq$UWt%G4IsW|K{V#1@H-K%8*4YO@+;^u3N{iZiStu^b7xAWWGux;5Z%umqqCV*< zdQ_?ygp4p+dlzJ>NXKYb4F1=4U{CRQ*SS)hg*(QUMN4A~U(Djx594T5n$ij1D~+GV ztb?RlMp!zwY+;b)C=1XEQKucrbv^hLDWdW-+DLiZ4B^FechXvi2fX18;ETUXM>id; z6O6YJO-n7(aWp!c%2ZA9*|S30x$X}?`J{3N?#fYTVm|31g0|8Vz(;6Yw$~>>h=1g+ z(o`SPdeg=8R(+JTzx6)s?b{qNO6{c(#S>YcP8!z3y_X4$nno!7*ozc6_(ODsGe>C z;J~TG-(lP*rR-X{;(GJ6Vq%A4Ec=2nMciTx zLGF)YNh5gHnFXB)Pk2CtcF1|Fcu(&?;ZO$erw>HhdqE!ZNP@-ORK708tR=0P1;XJ- z+H^(-vV%4JaxOi24>m5S+Q89B5T3*b0~0(q_5o^43pbbJr3x9lf6dRad30igD`7&m z{>f1Cu#i;5$mEaLarENLif_5$_EzPkd861fI+l}?1`CX;kX)#k?AqqpCMsJ~_r5d^ zGR7*Y zbj&5|IemFpgH({ZLLz=vk43gpCT9;7Ei+&Ux3X8phecQPSCOG$?zE+tkoAW_0(0ki zJ!xfz&@4hl;TLxpZ44}-qBLAZ>fR|8XaBW!;TVF%VrpYm!+L+D6BTZYr6Y*%m%6%n zM#Vit<0aS2_wL;-#rwbo&?CXLX7;YJD(c2*1Ag!i>61GDvA*D|w+!!{jOl%ypkE$9 zaL2lm*vk&jm!6;`1nNz7w3Krtr{uw>ok6q{Q_3PE?3M1}9&a4wb2LoM;X@jT&S62|Cf^wea1 zGI2kEEiUW*tq;#g<)=VyJPngS8;bnOwunSIqH#Got40u*d)nMLTz`I>4Aq8*V9FZR zDpeK=fvrb$7%0rSUYs>G6i8oujn3%DZ5$D>8I2$dRP^<$p|3Jtmw9lbWGVy(*hi&3 z{j38Vyk#<9Z2yFL?Jk)AEX^EtIJN23&F(p=`7(v>Vj^vS-iN|e7AdUF*JW@lu7v?_ z@e9DyqJ5h(>aWhwkHHK_BT-G|W4+p5O6GJ*$Xk?WSShF*!NAnygJX( z5K*pNLgp*~j1;APGnIe`^sK#mx7c2Q&oyk|d_De#f503+CAD@63CEv3j1jdKDs7WZ zwD?jRieROA$jv7$^Vck;72j)Hwukr5(1gZ zyRJ1ii-;=Fi9sEO`IQce|I9yYUms5c0E3gdU(8iKG-4iF9E_O8E!eE5gV%E;L9V-zye z_^S3Hu6D#TIbZ!$x^bcgBYyysB~uh|mI zRrfMo2I@6z->y-Y_-*?(p5s0cj?1#I`6-+0Mf+$HKGKC;T+dLzh+LVZ zCi0e=04jKGRE25}>yqv&-dMUF;jnSV(R%x^J7el~?1`$c8%Amp)Ij-`Sr~`=>ZI#1zIl2%WGGGBgh~IU5$CsweI*)5WVIjkDJe^e9X}>+4>-3@Z zdj)srN!Zn+am{#hu`7Fb(0GT=RcLmLfMM0o_`6Tr{*sK%XF5=mx z8<{9~g*ZAbyr>ojBB{CP%FFbJ|EZvxPOp@b$nKK>eRZ^udm16p^V{pK%$^F?Ad4>@ zqNdWl_*Z^6&UtrsXu~P-u8Ki|5L}s;zHhiF=HVu{rXZGehO94S9jW(40g5QD^(30f z45780YIuRK4vlutFL~sww8x zVMysK0_XDS1-q~J{M!#u0hVn8o|u^vNZDCp-zbLVKr9RU3Nb5d&Vb77`q7kAL6S8q z(p}>q3<>*`T0gL~l}nmMpFOO37YDv|;9gTi+hxIPT**yrwWw?F9>eF10z)O)^ZY(5 zKLF9{IRF5ODAoRLFwfVR3sMN>z*(Ft^Q_ji583l)1m5gXDl*hZC2Wyi>7z-W@NjA1 zO901M{TO&<=vma~^T-^hUKi+Pq=*)UD%*1N_RT+y3 zTR%JpuGR4t#C>>10;j#VZO!4t9F3I%=#7>UJv3DVc+~@5ROAF-v~Ovgy@#nWa6d8) z#O82B4s__R(H}|2&K42~a+9415Ru`k&pBQ8CTM)%g4PC+PH-V1!QKE!ndxaZ=06i) zz!>0Q;vmwhJk|vZGo6g)hDf+@sP=TTQwtwlAG$(l7JrQkNApWb8`+Bw?*nd_{`7dx zCe#ekn)=Wo>$)ZF zvCMBhgfmtnaa7%d%qpQQ@Mw#<@W=Y|s^|}g3nsWJ*Or^EXt_#w(8-aji0kx4fj=KU zA)K-}$udXz&)3Rk;xz_05#hVxX!(eU)T%FTbWR0;$Xy*Ni^2u|aGnr*zvHbXLDe67 zpYfHIqR+OTPuHw9iIwZv6^rFdTt6ZVNokPwO>G}0(r1NYVC zYuPMk-LV*rg*sy0Sj>*~h!&sNtWdl5odj<+t%b^4PCU3>u@;dxwnS+}vDKpN(mY{R zDFCbn7djBUyUe{4jLz=IV8;bPfkc{a-K}>!f&^EBnlif3t)JOJ$4&Cgdz!FuwU3gs zZZ<><_P+8LO?;5sKic~w^B!qGG728+@o^dgYD{6*F$gE5AeF-&XHF7(v2sVD*p~4C zP}XV}x&Vv=0U;Z+OnU`*63ze>Be9Lk^^Dz>3eo6qU8vB}Rk`-BaX(yLNl7u49|NC` zzGNMgnOd=@mGtjH+8v)XgdGGgBTtU=6-glZtUUL=sb~rQ-Ikw4aA?K-|EH9%jB2x6 z+75+Myto$EV!_>ldvPcZA!v{iD1qYcP~3{ULy_WEpjasmZ7EP3iWi6Sg+9;QbDp!# zx85Ibeq`-?@0n|6u9-b|*1fXt)e(MS7#Z3CsI66PF-#)&U-c!zqH}@F3PrAQB{vN*eyz&w?Makk}FS&C3dIQ5l=6okp0*B+h<~( zNN}m~OhW8^-P(ZM0l}DZkHG1#$jE@Sc!g(XN55R`usk>9yndZM)?wxl#)`fxWCtF! z(>#h}G_GA@1CFJxn!6h3V1YvnCVzGKdBu_8j({g0>iS3#6cZDA&T`NGg#57js971_ zs<8aD^+pN;%L!-an37d%|LN zq)}m7;Bgk2XiH|vbuB|*%HLMEPctX@WTKh9>-1?bqq$=~vm4`cTx#gf6^9aIalo#c zWx<{Ola}cT5PbNh2F>31%7}up@QAmxmq3JH>H5J-oub1Rh1(01J@1axTx;gGhD1^O zea2h818~D?MI;q5G2B{Q!bm7teo*3Mdkg6$X!8G@bOIh!)cEY=d8uJ_t}z~-9;7Rk z2ghISXpG3xpnAXJYoLwAzO1cH7Fz6Y@3#VtFGC|<^>kTUpKO){z-Puh+b>8nKbFh*TXAY;Wti=)2ZEd>58eWU4CZW+glU zLCqhot9tt6>fHcoMG=`9nTGSB9{s~Fd*w>wOwq`r*bX&jG3?$At*C-r>qV68iau_c zon)O4>AIHlyUWC#83)CA;pfmPU{?>oPCy8-&nsL+}2 z%&E5ZxEt-%;|;Bn@9Qd5_E-R%?7U}hDle1^PcEq_Kj`gdj9Tn__qvmQ(dK*dwRSl$ z0Nz==ji$d60Scwqba4U1qmrlk3eFaagtr;x#IjX-lT&~iY<(7$FGuY22tvvaojvyG z9EbQQeW;s)xeZG4vC);A+`n0TOA^0iOGlSl0Vc!z$P4_Fmiym!PZBti*(Dpv0s1{C(B-CuBk-I^^q?258yjk;3 zDOd;OSmRfd6}8g@k1Vgc2m^goNMnNDUUjBfTYmAFPYa?hk{Cg3c>2H@e*nsnylDop z=a=f3=7dsBW!eL=sQ&D4u7n4j444ZmTXZ9Mo-V~NIF?iS$a^-K&aYEhPp8!@1KkGSNC2fsn zR`af^UnKYw1uN;;!!vF<(+}UbQW$S*Hy&x%&XZ7|)7%(M?$VhjhY$)r^Au>;C z_M)y*9c~%GSf;7Y6JxV(i-VrKd3T>9u~%>L3qOGiL;pF!f=%4k$tCgL5HS~;66_;` zTenPt(vxs25IL&{%W#DQd1HDp%hv6AUFi&7EMWwd@+Cj-jGMW%R4}Hr`a}B0F6&cd zPxM$LHX^MLJ&=v3>rp3up#17>^3Q4$#10LF%NtKRf)jy)H*t5wr>>LMBR_+@Bb=1? zoFC_1(1bIfgYl7K!!qw1?}M$go>!zLJ%$Dm!S+YPGQKkGdK<-3^UkV~^1t{xG9n>+P$Y_Y3LH^$TC3!ml@?&)kKXFvKXo z^+$iNA~RkhwZZY^_}fuJKTTIWKUe7Sq!SF4OR2+#y#IBX1hh1NUUwY-!#@YD!8 zG1lHW{B&&4kQYGsMa(ymQx4PdnDp^lNATu~H8w^n%8(TgKSX9F*f8TWs^5TRF*O|v zbrO>bdc&_Yvl^^)A__;{ul6g#E-eZ`hLh)2%J|WpFT^Csi1=lR-e8c%Asr5U!mj=q zi?v9csN(~)NZ(h8;??t zYo%UBvn3C-5%*TSmVl##e_7Dx5#Pvs_v*KKB1QuDMc7SD6>y~Q_%ij?C{*+(F2dd}#qDE_KY@lN~Z$?Q85 zlYagpwrRc)uLv|y4vrgq^bMvwcO~~EWq>Jr*i7rwQuTygp)lpz^-s;BJkL1I&wT1| zzj0XzeGX_jt5C;>uJ_^WU1X5M>@Up%2u;A&<&O(mG&e47L1w_OKGi&a$zKUIO_A%a zkp#XNnZhcfm`QL&<03HV=qc}lPgv>nUlNWai1$Cy*NEA?LHx=F5X_&f!2D7`l9?vj z)v-+E)p&l(eNGilGY!Wx^E9l+m3}>i6ER+jRh!>z@)=VX{`E0&4rcPK>SaJ8OhO5i#`x=;62DatlVq+V} zs6O~DMwt!&ir*z~9)(Z40$PXe!+QL*Ke$AEnLiy0G;>iaU> zL9y2fkJw$YrdMJqm;Slj)et`ETD+76q#Xub(-&B^6gy2X+xY(RMvIDVl*$<(Tafn6nIV+ zxzZVcUYy=bxJ7DsH)&~yJAAw~ zP(8~Fv+yFqLz3xRdV-V<+z)$dTV9X+c1T1=b@xwj(v^!1GgVolZ4Aa%?_5Z?wD6SS z@B?>#M%B>M6B1J1R@FB}gRwZnr10dlE`P?LP>}5%Jdy$dB?q5Mb*4y1$Xc(-Kfnj zLZkgn8URrlwUP!B`r=>rDO?Vfkg4bcCxlrQHCziPUnpTi`{(a|PIQcUOs^DvK1`1o z?h%*hgp)~w9G{B1wqnZYJXDQ@7KefzZW8dFdqyy#1+66)OPfW_+dLqlT#ZqlvPm@1 zHb-St`8G@@EfKvhp?j`K}vF7_cZx4YaD{cID z*QN@L{AdeL7cLn<{+p_+p3_?pZz$_%3Gx=6iQSj^QRpo?af&?$QtzjN>cBQyri9Aci}p$&7N=h&FefWp|cWVU;aLAJf=& z^Ls$d^s)B=Ij3CWE0|uIIkAh+ukgX@)1j#T8aKFWsw*-NZF|({WNBWO75(IS^);vo z4R(vVCLD__WAw_Se@Np?&<@_|w&PDpCVtDLY3BI4MujrR&;%Q0c*`6x56*CiUn|1Q z`jIf0bP3<6Ph^s@bg2y8}40S zBCmp8(#xt7%jyLX?jf88$6i<`n}_TnNm*Oe6QCePSl<8%06>J!bR%9aXkxZD`2d+XAO^HtTV*(DNWMkivT)6`CB z)8~i;a+^a;<)#5PldN;hbyqAw7m6&ukZG#IkPH zT0cWRVnd(w=Q=~BVhdK*LDM?_Hy_v0woV@or4Hdd76ZWok5#{HCh4{}3tGKfq%N2M zSl}wwoTI!oh8*dRx7<||b z8h=kB^5b%VKVWb_BaXTr<5R|~8A+T@dOsz)mh*0pd|iec%paj&m2=0C9l}I zaD`EgKI`H<{qw7e&6e0?aTTMG(>34ZNq2Kq9L=2&kQ~=mVj~BQd#8u%(i8z5pv^(F zMBRZzAULljBT2<~c?yv!jbHW0E3~xn5+uv3?Y-d2KEeA%XdK z-6HEwIB`EY*N@)DuqjCm;Xnc0Q1%WP(pzlx^PbW*Weng{fZZx<@z11PoGH~a$j*_B z#akAST4~!vE?(@dpp55g4DOz9)kKqXszaGFl$Jmse`NM@xV~cJCo-MG>eq<3J(aDP zJPTMY)JK(Umk)g8)i3Sk+nUh3{Y0WBuCz+DYEmpQ0b+erSEdpU5^Q zAW;9#QDlOdW+~%oiELq7R(*H!#FcT25_=ZA)c57B>EpV=g6S*1Pxe)QR>D6dbw}jY z$qcl(r>P+u9mMe-Kg}buCm#CjD=}s%vs&%0V34JGVbeI=Q3-kK_`KRUB0rQzi&2$j zV6o_t8nlm`z|9%q8LQGy#!+xu!hp%iLyHVVr*Tg|Q4EDwLg%LazT*;ch-vKU#qkIT z`_L__M!xY*zVtm<*9#YOo*c-BA#L;4YDM0}4y-oC_cJMjIcsbktF=WpG&GvAUQp=^ z%8Ixy-NfoX%!}vmO%s{UHlK(Xs_ZD-}Z}r;?4G z%io5b)lAy7XDu7^AOORb5x9K}oSX*LMi{@=M3)d4rJ8)?Un{P_Zr;Jx>eH6 zck6cIy09HWJ&DwHl(~XBkME@r_N)kby7we;RLW1NMT~ItYr5jFOmDl&DaqQ!6Y(Cc z3=1~uca#x3A9%T>w{@r0vOf$+$K57?pQ8nn)6JY7Hc+4vk*(1m%!sqsZ-4e{OkO4E zXhoZD|Ge%~eD20Rxn_)9HqC*z?Wb3kApU_&_D2d{`R5=$`o~nvdIeFokG+GI7HQ%+ z3Ap1IKi<9IIef6oZk@@y@F_=d)urkO2k~lrGQ~D(mUeOc1w5kAB86M_X?!?Iy+>cA z`f>LXyLM%&_!=J^nlxb--{aq|GDWz7+4MQYV%F%fY7i;OL5M7sQU(=OP_DVFR#G=5 zeWn}k>X$gND}Im6i&+!qTp_Nq(Q%RTbu!Hgt;Z|m8|#4Et3xEukVY0D?d0uS`S_=1 zt`}rOLIs0L7E|xYo*BZ~l0*4wVc-=q(i1_3RCBfY3f15|!9lGzJJ5aB=T~NG*tvZ5 ztwNnIME513n}(!#v3^pBI~zLs4LC#s_t&;{UU{E54iEdU*RpRex@<9Pk={x$C5E<~ zl!r*;Z?XBLCqn7_HzFEwXa$~CY{TA%8@Gj6nrWKRR<4D3PKz|J>#@yb zoM0ls9Y%uZ#$%j#R3(>hVsn>;vEO?{wVUc6AoDF67wFlQ`Mkml(|PzS?s5u$VKHz} zxFE|Z(_zQ@!+=REFMG|JGE%!n=~aySA(kTTgt>HY0kWXl^Y#jZjlj#?4JHwvu-si@ z>&|KO<=MpJ;RkxsmM^CGC2VtGh97Y#8m9J!AEeNSNo0<4>-KUHF^&(Z7~8uGh%XC; zGWqz}v>Ij=OeWeVO+rsYR*rpGkPR#!eUjE#m96e28n}=O?{hY+M&Ys)hodm|bH3KO zCTlSMG@(Y+8^*-OhrMTEh^RbV61Y9oh2gtV%Os{v0quWZ%BO(Q4XpM zwmrrDodf_t5d&^r;$1dvpJ0)*Qlt(!1+ox<7oM>VM1*aoXIF7pt9m|oo|-fVKvE4F zaV~`t8R2`qEKbnqqnlphPCBy!XCYm&G%joFbMj6b-(4KzGbYBRJE(S`q4x4njIg`d(M3d`RxU1r%nrWm60A75=c) zZNz!C{wl-%gfv_n5WCLY5z}cRrwaG`vgR!|qE~)S4Wq_hGAzcP;Q6@$K=QEbM%-3?Ug@fN+d3#br};5+%>_%~`E15^HFhQks#nM!%@YdL!Rkuu z8z37ai?V`IX#3@1q$$uAbtC#nkC=C3zs4}y+ff@a`Ic+ z9+8#ktUX{edU5K=wIas}>?L z2N^{@jcW6cdWHP~4=bnWjt_F88Q;~Fl|}rzE#o*+A0+QprqIzN6?1DT7_U*1^L1?x z;(z@9fGoc@3Mv!GT_3Po!j(Esc&8!ASVt4BSe;J$qcb&yh-*Io85WTv5NjGr2@$=@ zu|xLeOSMRO$ru^NM)fVMR7Sl9Jl!UsUbIl}&N-F>mS7LosV!%v9Eua_#Of+;JH*|N zIWKImP$c8M^^wWdliq9D4cSgF%8ihW6rmwJMU5}+JmudxY-$A30B1@&MFczbE1QFm zw?>oRf2^nfi8w*TdQ-seX9niJWIf?JtcjP^(+&=p-dD#NZ-uM;Awkn*|Nr5$XgJfs4~wSUFolL=fZq zX&{{h2D+H5m9>bLto+|Hh%0eATQ@fs5g^dR!-La9oWI@S zLDX__tFqH!I-3OZ-;-9V!BV=5MBh_%Al$7Ju1t{Vjvg`a`dXj4Q;-4q@~+FEoDIy ztT-JvH?M%Opn$NDz6lb$1j; z6q5y_&Y3J6T}1DvVb1QZV2HSlv$G8p@@E!|ShldUaT34h1VKC<4WgW_z#?E*h=rT8 ztHkf3)rP$IYXG&AKwv^W5nm2s!28-m%=|U@Ed%^_O~lyhUzPe700`>#KS=kv5rJAb z*@)A@IjkVo7Vc0tI*I?fJ^#bhZ;*Ra{|=8Kc3%S`s&-(A6YRe@{(GmmSh&I + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60c0f7a..0f90c9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,5 +4,7 @@ Click here to see the latest things happening around the world Something went wrong No internet connection + No results found + Try adjusting your search to find what you are looking for Try again