diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..586195f --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/it/bz/noi/community/data/models/News.kt b/app/src/main/java/it/bz/noi/community/data/models/News.kt index 0ea8440..2ad972f 100644 --- a/app/src/main/java/it/bz/noi/community/data/models/News.kt +++ b/app/src/main/java/it/bz/noi/community/data/models/News.kt @@ -11,6 +11,8 @@ import it.bz.noi.community.utils.Utils import kotlinx.parcelize.Parcelize import java.util.* +private const val ID_TAG_IMPORTANT = "important" // ID of the "important" tag + @Keep @Parcelize data class News( @@ -27,7 +29,7 @@ data class News( @SerializedName("ODHTags") val tags: List? = null, @SerializedName("Highlight") - val highlighted: Boolean = false, + val isHighlighted: Boolean = false, ) : Parcelable @Keep @@ -68,21 +70,21 @@ data class NewsImage( val url: String? = null ) : Parcelable -fun News.getDetail(): Detail? { - return detail[Utils.getAppLanguage()] -} +/** + * Get the localized detail for the current app language. + */ +fun News.getLocalizedDetail(): Detail? = detail[Utils.getAppLanguage()] -fun News.getContactInfo(): ContactInfo? { - return contactInfo?.let { - it[Utils.getAppLanguage()] - } -} +/** + * Get the localized contact info for the current app language. + */ +fun News.getLocalizedContactInfo(): ContactInfo? = contactInfo?.get(Utils.getAppLanguage()) // FIXME -> WIP -fun News.hasImportantFlag(): Boolean { - val isImportant = tags != null && tags.filter { it.id == "important" }.isNotEmpty() - return isImportant || highlighted -} +/** + * Whether the news is important, that is, it has the "important" tag. + */ +val News.isImportant get(): Boolean = tags?.any { it.id == ID_TAG_IMPORTANT } ?: false data class NewsResponse( @SerializedName("TotalResults") diff --git a/app/src/main/java/it/bz/noi/community/data/models/NewsParams.kt b/app/src/main/java/it/bz/noi/community/data/models/NewsParams.kt index 044cc45..bbe3afb 100644 --- a/app/src/main/java/it/bz/noi/community/data/models/NewsParams.kt +++ b/app/src/main/java/it/bz/noi/community/data/models/NewsParams.kt @@ -4,6 +4,10 @@ package it.bz.noi.community.data.models +import it.bz.noi.community.utils.DateUtils +import it.bz.noi.community.utils.Utils +import java.util.Date + data class NewsParams( var startDate: String, var pageSize: Int = 10, @@ -12,8 +16,17 @@ data class NewsParams( var highlight: Boolean = false ) -fun NewsParams.getRawFilter(): String { - if (highlight) - return "eq(Highlight,\"true\")" - return "or(eq(Highlight,\"false\"),isnull(Highlight))" -} +/** + * Factory for creating [NewsParams]. + */ +fun NewsParams(nextPageNumber: Int, pageSize: Int, from: Date, moreHighlights: Boolean) = + NewsParams( + startDate = DateUtils.parameterDateFormatter().format(from), + pageSize = pageSize, + pageNumber = nextPageNumber, + language = Utils.getAppLanguage(), + highlight = moreHighlights + ) + +fun NewsParams.getRawFilter(): String = + if (highlight) "eq(Highlight,\"true\")" else "or(eq(Highlight,\"false\"),isnull(Highlight))" diff --git a/app/src/main/java/it/bz/noi/community/ui/MainViewModel.kt b/app/src/main/java/it/bz/noi/community/ui/MainViewModel.kt index 28deea7..575e141 100644 --- a/app/src/main/java/it/bz/noi/community/ui/MainViewModel.kt +++ b/app/src/main/java/it/bz/noi/community/ui/MainViewModel.kt @@ -33,6 +33,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import java.util.* +private const val PAGE_SIZE = 10 // How many news to load at once + /** * Factory for creating the MainViewModel */ @@ -242,7 +244,7 @@ class MainViewModel( filters= filterRepo.loadFilters() } - if (filters != null && filters.isNotEmpty()) + if (filters.isNotEmpty()) emit(Resource.success(data = filters)) else emit(Resource.error(data = null, message = "Filter loading: error occurred!")) @@ -256,8 +258,8 @@ class MainViewModel( loadNews() }.stateIn(viewModelScope, SharingStarted.Lazily, PagingData.empty()) - private fun loadNews(): Flow> = Pager(PagingConfig(pageSize = NewsPagingSource.PAGE_ITEMS)) { - NewsPagingSource(mainRepository) + private fun loadNews(): Flow> = Pager(PagingConfig(pageSize = PAGE_SIZE)) { + NewsPagingSource(PAGE_SIZE, mainRepository) }.flow.cachedIn(viewModelScope) fun refreshNews() { @@ -267,9 +269,8 @@ class MainViewModel( } object NewsTickerFlow { - val ticker = MutableSharedFlow(replay = 1).apply { + private val ticker = MutableSharedFlow(replay = 1).apply { tryEmit(Unit) } fun tick() = ticker.tryEmit(Unit) } - diff --git a/app/src/main/java/it/bz/noi/community/ui/newsDetails/NewsDetailsFragment.kt b/app/src/main/java/it/bz/noi/community/ui/newsDetails/NewsDetailsFragment.kt index 1069263..cdcd491 100644 --- a/app/src/main/java/it/bz/noi/community/ui/newsDetails/NewsDetailsFragment.kt +++ b/app/src/main/java/it/bz/noi/community/ui/newsDetails/NewsDetailsFragment.kt @@ -32,8 +32,8 @@ import it.bz.noi.community.data.api.ApiHelper import it.bz.noi.community.data.api.RetrofitBuilder import it.bz.noi.community.data.models.News import it.bz.noi.community.data.models.NewsImage -import it.bz.noi.community.data.models.getContactInfo -import it.bz.noi.community.data.models.getDetail +import it.bz.noi.community.data.models.getLocalizedContactInfo +import it.bz.noi.community.data.models.getLocalizedDetail import it.bz.noi.community.databinding.FragmentNewsDetailsBinding import it.bz.noi.community.databinding.VhHorizontalImageBinding import it.bz.noi.community.utils.Status @@ -41,17 +41,22 @@ import kotlinx.coroutines.Dispatchers import java.text.DateFormat -class NewsDetailsFragment: Fragment() { +class NewsDetailsFragment : Fragment() { private var _binding: FragmentNewsDetailsBinding? = null private val binding get() = _binding!! private val viewModel: NewsDetailViewModel by viewModels(factoryProducer = { - NewsDetailViewModelFactory(apiHelper = ApiHelper(RetrofitBuilder.opendatahubApiService, RetrofitBuilder.communityApiService), - this@NewsDetailsFragment) + NewsDetailViewModelFactory( + apiHelper = ApiHelper( + RetrofitBuilder.opendatahubApiService, + RetrofitBuilder.communityApiService + ), + this@NewsDetailsFragment + ) }) - private val df = DateFormat.getDateInstance(DateFormat.SHORT) + private val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -82,16 +87,18 @@ class NewsDetailsFragment: Fragment() { super.onViewCreated(view, savedInstanceState) viewModel.newsFlow.asLiveData(Dispatchers.Main).observe(viewLifecycleOwner) { - when(it.status) { + when (it.status) { Status.SUCCESS -> { binding.newsLoader.isVisible = false val news = it.data!! loadNewsData(news) } + Status.ERROR -> { binding.newsLoader.isVisible = false Toast.makeText(requireContext(), it.message, Toast.LENGTH_LONG).show() } + Status.LOADING -> { binding.newsLoader.isVisible = true } @@ -103,22 +110,23 @@ class NewsDetailsFragment: Fragment() { private fun loadNewsData(news: News) { setTransitionNames(news.id) - binding.date.text = df.format(news.date) + binding.date.text = dateFormat.format(news.date) - news.getDetail()?.let { detail -> + news.getLocalizedDetail()?.let { detail -> (requireActivity() as AppCompatActivity).supportActionBar?.title = detail.title binding.title.text = detail.title binding.shortText.text = detail.abstract - binding.longText.text = detail.text?.let{ Html.fromHtml(it, Html.FROM_HTML_MODE_LEGACY) } + binding.longText.text = + detail.text?.let { Html.fromHtml(it, Html.FROM_HTML_MODE_LEGACY) } binding.longText.movementMethod = LinkMovementMethod.getInstance() } var isExternalLink = false var isEmail = false - val contactInfo = news.getContactInfo() + val contactInfo = news.getLocalizedContactInfo() if (contactInfo == null) { - binding.publisher.text = "N/D" + binding.publisher.text = "N/D" //TODO: localize this } else { binding.publisher.text = contactInfo.publisher @@ -169,9 +177,10 @@ class NewsDetailsFragment: Fragment() { binding.askQuestion.isVisible = isEmail binding.footer.isVisible = isExternalLink || isEmail - if (news.images != null && news.images.isNotEmpty()) { + if (!news.images.isNullOrEmpty()) { binding.images.isVisible = true - binding.images.layoutManager = LinearLayoutManager(binding.root.context, LinearLayoutManager.HORIZONTAL, false) + binding.images.layoutManager = + LinearLayoutManager(binding.root.context, LinearLayoutManager.HORIZONTAL, false) binding.images.adapter = NewsImagesAdapter(news.images) } else { binding.images.isVisible = false @@ -190,7 +199,7 @@ class NewsDetailsFragment: Fragment() { private fun writeEmail(receiverAddress: String) { val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse("mailto:") // only email apps should handle this - putExtra(Intent.EXTRA_EMAIL, Array(1) {receiverAddress}) + putExtra(Intent.EXTRA_EMAIL, Array(1) { receiverAddress }) } if (intent.resolveActivity(requireContext().packageManager) != null) { startActivity(intent) @@ -198,25 +207,27 @@ class NewsDetailsFragment: Fragment() { } private fun openExternalLink(url: String) { - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(url) - } + val intent = + Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER).apply { + data = Uri.parse(url) + } if (intent.resolveActivity(requireContext().packageManager) != null) { startActivity(intent) } } - } /** * Adapter used to populate the image gallery of news detail */ -class NewsImagesAdapter(private val images: List) : RecyclerView.Adapter() { +class NewsImagesAdapter(private val images: List) : + RecyclerView.Adapter() { /** * View holder of a single picture */ - inner class NewsImageViewHolder(private val binding: VhHorizontalImageBinding) : RecyclerView.ViewHolder(binding.root) { + inner class NewsImageViewHolder(private val binding: VhHorizontalImageBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind(image: NewsImage) { Glide @@ -228,7 +239,13 @@ class NewsImagesAdapter(private val images: List) : RecyclerView.Adap } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsImageViewHolder { - return NewsImageViewHolder(VhHorizontalImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + return NewsImageViewHolder( + VhHorizontalImageBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) } override fun onBindViewHolder(holder: NewsImageViewHolder, position: Int) { diff --git a/app/src/main/java/it/bz/noi/community/ui/today/news/NewsFragment.kt b/app/src/main/java/it/bz/noi/community/ui/today/news/NewsFragment.kt index 47fd6a4..0d122e0 100644 --- a/app/src/main/java/it/bz/noi/community/ui/today/news/NewsFragment.kt +++ b/app/src/main/java/it/bz/noi/community/ui/today/news/NewsFragment.kt @@ -30,9 +30,9 @@ import com.bumptech.glide.Glide import it.bz.noi.community.data.api.ApiHelper import it.bz.noi.community.data.api.RetrofitBuilder import it.bz.noi.community.data.models.News -import it.bz.noi.community.data.models.getContactInfo -import it.bz.noi.community.data.models.getDetail -import it.bz.noi.community.data.models.hasImportantFlag +import it.bz.noi.community.data.models.getLocalizedContactInfo +import it.bz.noi.community.data.models.getLocalizedDetail +import it.bz.noi.community.data.models.isImportant import it.bz.noi.community.data.repository.JsonFilterRepository import it.bz.noi.community.databinding.FragmentNewsBinding import it.bz.noi.community.databinding.VhNewsBinding @@ -197,14 +197,16 @@ class NewsVH(private val binding: VhNewsBinding, detailListener: NewsDetailListe fun bind(news: News) { this.news = news binding.date.text = df.format(news.date) - binding.importantTag.isVisible = news.hasImportantFlag() - if (news.highlighted) // FIXME -> WIP + binding.importantTag.isVisible = news.isImportant || news.isHighlighted + if (news.isHighlighted) { // FIXME -> WIP binding.importantTag.text = "TMP PINNATO" - news.getDetail()?.let { detail -> + } + + news.getLocalizedDetail()?.let { detail -> binding.title.text = detail.title binding.shortText.text = detail.abstract } - val contactInfo = news.getContactInfo() + val contactInfo = news.getLocalizedContactInfo() if (contactInfo == null) { binding.publisher.text = "N/D" } else { diff --git a/app/src/main/java/it/bz/noi/community/ui/today/news/NewsPagingSource.kt b/app/src/main/java/it/bz/noi/community/ui/today/news/NewsPagingSource.kt index 9a0e7fe..c6e73c8 100644 --- a/app/src/main/java/it/bz/noi/community/ui/today/news/NewsPagingSource.kt +++ b/app/src/main/java/it/bz/noi/community/ui/today/news/NewsPagingSource.kt @@ -15,22 +15,20 @@ import it.bz.noi.community.utils.DateUtils import it.bz.noi.community.utils.Utils import java.util.* -class NewsPagingSource(private val mainRepository: MainRepository) : - PagingSource() { - - companion object { - private const val TAG = "NewsPagingSource" - const val PAGE_ITEMS = 10 - } +private const val TAG = "NewsPagingSource" +class NewsPagingSource(private val pageSize: Int, private val mainRepository: MainRepository) : + PagingSource() { + private val startDate = Date() + private var moreHighlight = true override suspend fun load(params: LoadParams): LoadResult { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 - val newsParams = getNewsParams(nextPageNumber) + val newsParams = NewsParams(nextPageNumber, pageSize, startDate, moreHighlight) return try { val newsResponse = mainRepository.getNews(newsParams) @@ -42,7 +40,12 @@ class NewsPagingSource(private val mainRepository: MainRepository) : if (moreHighlight && nextKey == null) { moreHighlight = false - val notHighlightedNewsParams = getNewsParams(nextPageNumber) + val notHighlightedNewsParams = NewsParams( + nextPageNumber, + pageSize, + startDate, + false + ) val notHighlightedNewsResponse = mainRepository.getNews(notHighlightedNewsParams) news.addAll(notHighlightedNewsResponse.news) @@ -59,17 +62,8 @@ class NewsPagingSource(private val mainRepository: MainRepository) : FirebaseCrashlytics.getInstance().recordException(ex) LoadResult.Error(ex) } - } - private fun getNewsParams(nextPageNumber: Int) = NewsParams( - startDate = DateUtils.parameterDateFormatter().format(startDate), - pageSize = PAGE_ITEMS, - pageNumber = nextPageNumber, - language = Utils.getAppLanguage(), - highlight = moreHighlight - ) - override fun getRefreshKey(state: PagingState): Int? { // Try to find the page key of the closest page to anchorPosition, from // either the prevKey or the nextKey, but you need to handle nullability