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 # Android Studio generated files and folders
captures/ captures/
app/phone/libre/release/baselineProfiles
.kotlin .kotlin
.externalNativeBuild/ .externalNativeBuild/
.cxx/ .cxx/

View file

@ -16,36 +16,10 @@
} }
], ],
"attributes": [], "attributes": [],
"versionCode": 4, "versionCode": 5,
"versionName": "0.14.2", "versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-armeabi-v7a.apk" "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", "type": "ONE_OF_MANY",
"filters": [ "filters": [
@ -55,9 +29,35 @@
} }
], ],
"attributes": [], "attributes": [],
"versionCode": 4, "versionCode": 5,
"versionName": "0.14.2", "versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86.apk" "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", "elementType": "File",
@ -67,9 +67,9 @@
"maxApi": 30, "maxApi": 30,
"baselineProfiles": [ "baselineProfiles": [
"baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm", "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-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, "maxApi": 2147483647,
"baselineProfiles": [ "baselineProfiles": [
"baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm", "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-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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -16,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.adapters.EpisodeListAdapter import com.nomadics9.ananas.adapters.EpisodeListAdapter
import com.nomadics9.ananas.databinding.FragmentSeasonBinding import com.nomadics9.ananas.databinding.FragmentSeasonBinding
import com.nomadics9.ananas.dialogs.ErrorDialogFragment import com.nomadics9.ananas.dialogs.ErrorDialogFragment
import com.nomadics9.ananas.dialogs.getStorageSelectionDialog
import com.nomadics9.ananas.models.FindroidEpisode import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.utils.checkIfLoginRequired import com.nomadics9.ananas.utils.checkIfLoginRequired
import com.nomadics9.ananas.viewmodels.SeasonEvent import com.nomadics9.ananas.viewmodels.SeasonEvent
@ -23,6 +29,11 @@ import com.nomadics9.ananas.viewmodels.SeasonViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nomadics9.ananas.models.UiText
@AndroidEntryPoint @AndroidEntryPoint
class SeasonFragment : Fragment() { class SeasonFragment : Fragment() {
@ -31,6 +42,7 @@ class SeasonFragment : Fragment() {
private val args: SeasonFragmentArgs by navArgs() private val args: SeasonFragmentArgs by navArgs()
private lateinit var errorDialog: ErrorDialogFragment private lateinit var errorDialog: ErrorDialogFragment
private lateinit var downloadPreparingDialog: AlertDialog
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -38,6 +50,39 @@ class SeasonFragment : Fragment() {
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
): View { ): View {
binding = FragmentSeasonBinding.inflate(inflater, container, false) 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 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 { launch {
viewModel.eventsChannelFlow.collect { event -> viewModel.eventsChannelFlow.collect { event ->
when (event) { when (event) {
@ -110,6 +174,47 @@ class SeasonFragment : Fragment() {
checkIfLoginRequired(uiState.error.message) 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) { private fun navigateToEpisodeBottomSheetFragment(episode: FindroidEpisode) {
findNavController().navigate( findNavController().navigate(
SeasonFragmentDirections.actionSeasonFragmentToEpisodeBottomSheetFragment( SeasonFragmentDirections.actionSeasonFragmentToEpisodeBottomSheetFragment(

View file

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

View file

@ -15,15 +15,29 @@ import org.jellyfin.sdk.model.api.ItemFields
import java.util.UUID import java.util.UUID
import javax.inject.Inject 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 @HiltViewModel
class SeasonViewModel class SeasonViewModel
@Inject @Inject
constructor( constructor(
private val jellyfinRepository: JellyfinRepository, private val jellyfinRepository: JellyfinRepository,
private val downloader: Downloader,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading) private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow() 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>() private val eventsChannel = Channel<SeasonEvent>()
val eventsChannelFlow = eventsChannel.receiveAsFlow() 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 { private suspend fun getSeason(seasonId: UUID): FindroidSeason {
return jellyfinRepository.getSeason(seasonId) 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="alaskarTV_requests">AlaskarTV Requests</string>
<string name="pref_player_trickplay_gesture">Trick Play in seek gesture</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="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> </resources>