From d4aca3e0896ca20d00c2d78d1fea9deb0a56eb22 Mon Sep 17 00:00:00 2001 From: Christine Coenen Date: Thu, 10 Nov 2022 11:41:51 +0100 Subject: [PATCH] Fill download list --- .../code/zapp/app/KoinModules.kt | 2 +- .../zapp/app/personal/PersonalFragment.kt | 15 +++ .../zapp/app/personal/PersonalViewModel.kt | 4 +- .../adapter/DownloadItemViewHolder.kt | 127 ++++++++++++++++++ .../personal/adapter/DownloadListAdapter.kt | 58 ++++++++ .../code/zapp/persistence/MediathekShowDao.kt | 3 + .../zapp/repositories/MediathekRepository.kt | 12 +- 7 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadItemViewHolder.kt create mode 100644 app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadListAdapter.kt diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/KoinModules.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/KoinModules.kt index 5bf1ad1e..e51c8765 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/app/KoinModules.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/KoinModules.kt @@ -64,7 +64,7 @@ class KoinModules { viewModel { AbstractPlayerActivityViewModel(get()) } viewModel { ChannelPlayerActivityViewModel(get()) } - viewModel { PersonalViewModel() } + viewModel { PersonalViewModel(get()) } viewModel { DownloadsViewModel(get(), get()) } viewModel { ProgramInfoViewModel(androidApplication(), get()) } viewModel { MediathekListFragmentViewModel(get()) } diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalFragment.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalFragment.kt index f19c3786..ce49512c 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalFragment.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalFragment.kt @@ -5,10 +5,13 @@ import android.view.* import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import de.christinecoenen.code.zapp.R +import de.christinecoenen.code.zapp.app.personal.adapter.DownloadListAdapter import de.christinecoenen.code.zapp.app.personal.adapter.HeaderAdapater import de.christinecoenen.code.zapp.databinding.PersonalFragmentBinding +import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel class PersonalFragment : Fragment(), MenuProvider { @@ -17,13 +20,19 @@ class PersonalFragment : Fragment(), MenuProvider { private val binding: PersonalFragmentBinding get() = _binding!! private val viewModel: PersonalViewModel by viewModel() + private lateinit var outerAdapter: ConcatAdapter + private lateinit var downloadsAdapter: DownloadListAdapter + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + downloadsAdapter = DownloadListAdapter(lifecycleScope) + outerAdapter = ConcatAdapter( HeaderAdapater(R.string.activity_main_tab_downloads), + downloadsAdapter ) } @@ -36,6 +45,12 @@ class PersonalFragment : Fragment(), MenuProvider { binding.list.adapter = outerAdapter + lifecycleScope.launch { + viewModel.downloadsFlow.collect { + downloadsAdapter.setShows(it) + } + } + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) return binding.root diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalViewModel.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalViewModel.kt index 3f3f16b1..3792b465 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalViewModel.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/PersonalViewModel.kt @@ -1,9 +1,11 @@ package de.christinecoenen.code.zapp.app.personal import androidx.lifecycle.ViewModel +import de.christinecoenen.code.zapp.repositories.MediathekRepository -class PersonalViewModel : ViewModel() { +class PersonalViewModel(mediathekRepository: MediathekRepository) : ViewModel() { + val downloadsFlow = mediathekRepository.getDownloads(3) } diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadItemViewHolder.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadItemViewHolder.kt new file mode 100644 index 00000000..88de0960 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadItemViewHolder.kt @@ -0,0 +1,127 @@ +package de.christinecoenen.code.zapp.app.personal.adapter + +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import de.christinecoenen.code.zapp.R +import de.christinecoenen.code.zapp.databinding.MediathekListFragmentItemBinding +import de.christinecoenen.code.zapp.models.shows.DownloadStatus +import de.christinecoenen.code.zapp.models.shows.MediathekShow +import de.christinecoenen.code.zapp.repositories.MediathekRepository +import de.christinecoenen.code.zapp.utils.system.ColorHelper.themeColor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class DownloadItemViewHolder( + private val binding: MediathekListFragmentItemBinding +) : RecyclerView.ViewHolder(binding.root), KoinComponent { + + private val mediathekRepository: MediathekRepository by inject() + + private val bgColorDefault = binding.root.context.themeColor(R.attr.backgroundColor) + private val bgColorHighlight by lazy { binding.root.context.themeColor(R.attr.colorSurface) } + + private var isRelevantForUserJob: Job? = null + private var downloadProgressJob: Job? = null + private var downloadStatusJob: Job? = null + private var playbackPositionJob: Job? = null + + suspend fun setShow(show: MediathekShow) = withContext(Dispatchers.Main) { + binding.root.visibility = View.GONE + + isRelevantForUserJob?.cancel() + downloadProgressJob?.cancel() + downloadStatusJob?.cancel() + playbackPositionJob?.cancel() + + binding.title.text = show.title + binding.topic.text = show.topic + // fix layout_constraintWidth_max not be applied correctly + binding.topic.requestLayout() + + binding.duration.text = show.formattedDuration + binding.channel.text = show.channel + binding.time.text = show.formattedTimestamp + binding.subtitle.isVisible = show.hasSubtitle + binding.subtitleDivider.isVisible = show.hasSubtitle + + binding.downloadProgress.isVisible = false + binding.downloadProgressIcon.isVisible = false + binding.downloadStatusIcon.isVisible = false + binding.viewingStatus.isVisible = false + binding.viewingProgress.isVisible = false + + binding.root.setBackgroundColor(bgColorDefault) + + binding.root.visibility = View.VISIBLE + + isRelevantForUserJob = launch { getIsRelevantForUserFlow(show) } + downloadProgressJob = launch { updateDownloadProgressFlow(show) } + downloadStatusJob = launch { updateDownloadStatusFlow(show) } + playbackPositionJob = launch { updatePlaybackPositionPercent(show) } + } + + private suspend fun getIsRelevantForUserFlow(show: MediathekShow) { + mediathekRepository + .getIsRelevantForUser(show.apiId) + .collectLatest(::updateIsRelevantForUser) + } + + private suspend fun updateDownloadProgressFlow(show: MediathekShow) { + mediathekRepository + .getDownloadProgress(show.apiId) + .collectLatest(::updateDownloadProgress) + } + + private suspend fun updateDownloadStatusFlow(show: MediathekShow) { + mediathekRepository + .getDownloadStatus(show.apiId) + .collectLatest(::updateDownloadStatus) + } + + private suspend fun updatePlaybackPositionPercent(show: MediathekShow) { + mediathekRepository + .getPlaybackPositionPercent(show.apiId) + .collectLatest(::updatePlaybackPositionPercent) + } + + private fun updateIsRelevantForUser(isRelevant: Boolean) { + binding.root.setBackgroundColor(if (isRelevant) bgColorHighlight else bgColorDefault) + } + + private fun updateDownloadProgress(progress: Int) { + binding.downloadProgress.progress = progress + } + + private fun updateDownloadStatus(status: DownloadStatus) { + binding.downloadStatusIcon.isVisible = status == DownloadStatus.FAILED || + status == DownloadStatus.COMPLETED + + binding.downloadStatusIcon.setImageResource( + when (status) { + DownloadStatus.COMPLETED -> R.drawable.ic_baseline_save_alt_24 + DownloadStatus.FAILED -> R.drawable.ic_outline_warning_amber_24 + else -> 0 + } + ) + + binding.downloadProgress.isVisible = status == DownloadStatus.QUEUED || + status == DownloadStatus.DOWNLOADING || + status == DownloadStatus.PAUSED || + status == DownloadStatus.ADDED + binding.downloadProgressIcon.isVisible = binding.downloadProgress.isVisible + + binding.downloadProgress.isIndeterminate = status != DownloadStatus.DOWNLOADING + } + + private fun updatePlaybackPositionPercent(percent: Float) { + binding.viewingStatus.isVisible = percent > 0 + binding.viewingProgress.progress = (percent * binding.viewingProgress.max).toInt() + binding.viewingProgress.isVisible = percent > 0 + } +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadListAdapter.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadListAdapter.kt new file mode 100644 index 00000000..60b834fa --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/personal/adapter/DownloadListAdapter.kt @@ -0,0 +1,58 @@ +package de.christinecoenen.code.zapp.app.personal.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.recyclerview.widget.RecyclerView +import de.christinecoenen.code.zapp.databinding.MediathekListFragmentItemBinding +import de.christinecoenen.code.zapp.models.shows.PersistedMediathekShow +import kotlinx.coroutines.launch + +// TODO: use another, more spacialized viewholder +class DownloadListAdapter( + private val scope: LifecycleCoroutineScope, + private val listener: Listener? = null +) : RecyclerView.Adapter() { + + private var persistedShows = mutableListOf() + + public fun setShows(shows: List) { + persistedShows = shows.toMutableList() + // TODO: use diff util + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadItemViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = MediathekListFragmentItemBinding.inflate(layoutInflater, parent, false) + val holder = DownloadItemViewHolder(binding) + + binding.root.setOnClickListener { + listener?.onShowClicked(persistedShows[holder.bindingAdapterPosition]) + } + + binding.root.setOnLongClickListener { + listener?.onShowLongClicked( + persistedShows[holder.bindingAdapterPosition], + binding.root + ) + true + } + + return holder + } + + override fun onBindViewHolder(holder: DownloadItemViewHolder, position: Int) { + scope.launch { + holder.setShow(persistedShows[holder.bindingAdapterPosition].mediathekShow) + } + } + + override fun getItemCount() = persistedShows.size + + interface Listener { + fun onShowClicked(show: PersistedMediathekShow) + fun onShowLongClicked(show: PersistedMediathekShow, view: View) + } +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/persistence/MediathekShowDao.kt b/app/src/main/java/de/christinecoenen/code/zapp/persistence/MediathekShowDao.kt index 02207494..c9d3902d 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/persistence/MediathekShowDao.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/persistence/MediathekShowDao.kt @@ -17,6 +17,9 @@ interface MediathekShowDao { @Query("SELECT * FROM PersistedMediathekShow WHERE (downloadStatus IN (1,2,3,4,6,9)) AND (topic LIKE :searchQuery OR title LIKE :searchQuery) ORDER BY downloadedAt DESC") fun getAllDownloads(searchQuery: String): PagingSource + @Query("SELECT * FROM PersistedMediathekShow WHERE (downloadStatus IN (1,2,3,4,6,9)) ORDER BY downloadedAt DESC LIMIT :limit") + fun getDownloads(limit: Int): Flow> + @Query("SELECT * FROM PersistedMediathekShow WHERE id=:id") fun getFromId(id: Int): Flow diff --git a/app/src/main/java/de/christinecoenen/code/zapp/repositories/MediathekRepository.kt b/app/src/main/java/de/christinecoenen/code/zapp/repositories/MediathekRepository.kt index 85c101bb..d81c443c 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/repositories/MediathekRepository.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/repositories/MediathekRepository.kt @@ -12,8 +12,18 @@ import org.joda.time.DateTime class MediathekRepository(private val database: Database) { + fun getDownloads(limit: Int): Flow> { + return database + .mediathekShowDao() + .getDownloads(limit) + .distinctUntilChanged() + .flowOn(Dispatchers.IO) + } + fun getDownloads(searchQuery: String): PagingSource { - return database.mediathekShowDao().getAllDownloads("%$searchQuery%") + return database + .mediathekShowDao() + .getAllDownloads("%$searchQuery%") } suspend fun persistOrUpdateShow(show: MediathekShow): Flow =