feat: Download Season + Download unwatched episodes only dialogue

This commit is contained in:
nomadics9 2024-07-02 17:10:59 +03:00
parent 9baa84e1e7
commit e987ac477d
15 changed files with 186 additions and 34 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ local.properties
# Android Studio generated files and folders
captures/
app/phone/libre/release/baselineProfiles
.kotlin
.externalNativeBuild/
.cxx/

View file

@ -16,36 +16,10 @@
}
],
"attributes": [],
"versionCode": 4,
"versionCode": 5,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-armeabi-v7a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 4,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86_64.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 4,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-arm64-v8a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
@ -55,9 +29,35 @@
}
],
"attributes": [],
"versionCode": 4,
"versionCode": 5,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 5,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-arm64-v8a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 5,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86_64.apk"
}
],
"elementType": "File",
@ -67,9 +67,9 @@
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-x86.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-x86.dm"
"baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm"
]
},
{
@ -77,9 +77,9 @@
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-x86.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-x86.dm"
"baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm"
]
}
],

View file

@ -2,8 +2,13 @@ package com.nomadics9.ananas.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@ -16,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.adapters.EpisodeListAdapter
import com.nomadics9.ananas.databinding.FragmentSeasonBinding
import com.nomadics9.ananas.dialogs.ErrorDialogFragment
import com.nomadics9.ananas.dialogs.getStorageSelectionDialog
import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.utils.checkIfLoginRequired
import com.nomadics9.ananas.viewmodels.SeasonEvent
@ -23,6 +29,11 @@ import com.nomadics9.ananas.viewmodels.SeasonViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nomadics9.ananas.models.UiText
@AndroidEntryPoint
class SeasonFragment : Fragment() {
@ -31,6 +42,7 @@ class SeasonFragment : Fragment() {
private val args: SeasonFragmentArgs by navArgs()
private lateinit var errorDialog: ErrorDialogFragment
private lateinit var downloadPreparingDialog: AlertDialog
override fun onCreateView(
inflater: LayoutInflater,
@ -38,6 +50,39 @@ class SeasonFragment : Fragment() {
savedInstanceState: Bundle?,
): View {
binding = FragmentSeasonBinding.inflate(inflater, container, false)
val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(com.nomadics9.ananas.core.R.menu.season_menu, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
com.nomadics9.ananas.core.R.id.action_download_season -> {
if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) {
val storageDialog = getStorageSelectionDialog(
requireContext(),
onItemSelected = { storageIndex ->
createEpisodesToDownloadDialog(storageIndex)
},
onCancel = {
},
)
viewModel.download()
return true
}
createEpisodesToDownloadDialog()
return true
}
else -> false
}
}
},
viewLifecycleOwner,
Lifecycle.State.RESUMED,
)
return binding.root
}
@ -57,6 +102,25 @@ class SeasonFragment : Fragment() {
}
}
launch {
viewModel.downloadStatus.collect { (status, progress) ->
when (status) {
10 -> {
downloadPreparingDialog.dismiss()
}
}
}
}
launch {
viewModel.downloadError.collect { uiText ->
createErrorDialog(uiText)
}
}
launch {
viewModel.eventsChannelFlow.collect { event ->
when (event) {
@ -110,6 +174,47 @@ class SeasonFragment : Fragment() {
checkIfLoginRequired(uiState.error.message)
}
private fun createDownloadPreparingDialog() {
val builder = MaterialAlertDialogBuilder(requireContext())
downloadPreparingDialog = builder
.setTitle(com.nomadics9.ananas.core.R.string.preparing_download)
.setView(com.nomadics9.ananas.R.layout.preparing_download_dialog)
.setCancelable(false)
.create()
downloadPreparingDialog.show()
}
private fun createErrorDialog(uiText: UiText) {
val builder = MaterialAlertDialogBuilder(requireContext())
builder
.setTitle(com.nomadics9.ananas.core.R.string.downloading_error)
.setMessage(uiText.asString(requireContext().resources))
.setPositiveButton(getString(com.nomadics9.ananas.core.R.string.close)) { _, _ ->
}
builder.show()
}
private fun createEpisodesToDownloadDialog(storageIndex: Int = 0) {
val builder = MaterialAlertDialogBuilder(requireContext())
val dialog = builder
.setTitle(com.nomadics9.ananas.core.R.string.download_season_dialog_title)
.setMessage(com.nomadics9.ananas.core.R.string.download_season_dialog_question)
.setPositiveButton(com.nomadics9.ananas.core.R.string.download_season_dialog_download_all) { _, _ ->
createDownloadPreparingDialog()
viewModel.download(storageIndex = storageIndex, downloadWatched = true)
}
.setNegativeButton(com.nomadics9.ananas.core.R.string.download_season_dialog_download_unwatched) { _, _ ->
createDownloadPreparingDialog()
viewModel.download(storageIndex = storageIndex, downloadWatched = false)
}
.create()
dialog.show()
}
private fun navigateToEpisodeBottomSheetFragment(episode: FindroidEpisode) {
findNavController().navigate(
SeasonFragmentDirections.actionSeasonFragmentToEpisodeBottomSheetFragment(

View file

@ -1,7 +1,7 @@
import org.gradle.api.JavaVersion
object Versions {
const val appCode = 4
const val appCode = 5
const val appName = "0.14.2"
const val compileSdk = 34

View file

@ -15,15 +15,29 @@ import org.jellyfin.sdk.model.api.ItemFields
import java.util.UUID
import javax.inject.Inject
import com.nomadics9.ananas.models.UiText
import com.nomadics9.ananas.utils.Downloader
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlin.random.Random
@HiltViewModel
class SeasonViewModel
@Inject
constructor(
private val jellyfinRepository: JellyfinRepository,
) : ViewModel() {
private val downloader: Downloader,
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
private val _downloadStatus = MutableStateFlow(Pair(0, 0))
val downloadStatus = _downloadStatus.asStateFlow()
private val _downloadError = MutableSharedFlow<UiText>()
val downloadError = _downloadError.asSharedFlow()
private val eventsChannel = Channel<SeasonEvent>()
val eventsChannelFlow = eventsChannel.receiveAsFlow()
@ -51,6 +65,24 @@ constructor(
}
}
fun download(sourceIndex: Int = 0, storageIndex: Int = 0, downloadWatched: Boolean = false) {
viewModelScope.launch {
for (episode in jellyfinRepository.getEpisodes(season.seriesId, season.id)) {
val item = jellyfinRepository.getEpisode(episode.id)
if (item.played && !downloadWatched) {
continue }
val result = downloader.downloadItem(item, item.sources[sourceIndex].id, storageIndex)
if (result.second != null) {
_downloadError.emit(result.second!!)
break
}
}
// Send one time signal to fragment that the download has been initiated
_downloadStatus.emit(Pair(10, Random.nextInt()))
}
}
private suspend fun getSeason(seasonId: UUID): FindroidSeason {
return jellyfinRepository.getSeason(seasonId)
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_download_season"
android:icon="@drawable/ic_download"
android:title="@string/download_season_button_description"
app:showAsAction="always" />
</menu>

View file

@ -195,4 +195,9 @@
<string name="alaskarTV_requests">AlaskarTV Requests</string>
<string name="pref_player_trickplay_gesture">Trick Play in seek gesture</string>
<string name="pref_player_trickplay_gesture_summary">Requires \'Seek gesture\' and \'Trick Play\'</string>
<string name="download_season_button_description">Download season</string>
<string name="download_season_dialog_title">Download Season</string>
<string name="download_season_dialog_question">Which episodes do you want to download?</string>
<string name="download_season_dialog_download_all">All Episodes</string>
<string name="download_season_dialog_download_unwatched">Unwatched Episodes</string>
</resources>