diff --git a/.gitignore b/.gitignore index 1b800698..73ee5927 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ local.properties # Android Studio generated files and folders captures/ +app/phone/libre/release/baselineProfiles .kotlin .externalNativeBuild/ .cxx/ diff --git a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm index bf90fd22..29f7dc6a 100644 Binary files a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm and b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm differ diff --git a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm index bf90fd22..29f7dc6a 100644 Binary files a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm and b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm differ diff --git a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86.dm b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86.dm index bf90fd22..29f7dc6a 100644 Binary files a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86.dm and b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86.dm differ diff --git a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm index bf90fd22..29f7dc6a 100644 Binary files a/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm and b/app/phone/libre/release/baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm differ diff --git a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm index 426d3b8e..301e21af 100644 Binary files a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm and b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm differ diff --git a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm index 426d3b8e..301e21af 100644 Binary files a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm and b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm differ diff --git a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86.dm b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86.dm index 426d3b8e..301e21af 100644 Binary files a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86.dm and b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86.dm differ diff --git a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm index 426d3b8e..301e21af 100644 Binary files a/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm and b/app/phone/libre/release/baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm differ diff --git a/app/phone/libre/release/output-metadata.json b/app/phone/libre/release/output-metadata.json index fd96b5fd..eac405bd 100644 --- a/app/phone/libre/release/output-metadata.json +++ b/app/phone/libre/release/output-metadata.json @@ -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" ] } ], diff --git a/app/phone/src/main/java/com/nomadics9/ananas/fragments/SeasonFragment.kt b/app/phone/src/main/java/com/nomadics9/ananas/fragments/SeasonFragment.kt index bcd07f62..b53e6127 100644 --- a/app/phone/src/main/java/com/nomadics9/ananas/fragments/SeasonFragment.kt +++ b/app/phone/src/main/java/com/nomadics9/ananas/fragments/SeasonFragment.kt @@ -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( diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 95652d8d..e9fe7b08 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -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 diff --git a/core/src/main/java/com/nomadics9/ananas/viewmodels/SeasonViewModel.kt b/core/src/main/java/com/nomadics9/ananas/viewmodels/SeasonViewModel.kt index 805ec535..a1a7e68b 100644 --- a/core/src/main/java/com/nomadics9/ananas/viewmodels/SeasonViewModel.kt +++ b/core/src/main/java/com/nomadics9/ananas/viewmodels/SeasonViewModel.kt @@ -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.Loading) val uiState = _uiState.asStateFlow() + + private val _downloadStatus = MutableStateFlow(Pair(0, 0)) + val downloadStatus = _downloadStatus.asStateFlow() + + private val _downloadError = MutableSharedFlow() + val downloadError = _downloadError.asSharedFlow() + private val eventsChannel = Channel() 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) } diff --git a/core/src/main/res/menu/season_menu.xml b/core/src/main/res/menu/season_menu.xml new file mode 100644 index 00000000..51b472b7 --- /dev/null +++ b/core/src/main/res/menu/season_menu.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 8661bb84..f7ec0340 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -195,4 +195,9 @@ AlaskarTV Requests Trick Play in seek gesture Requires \'Seek gesture\' and \'Trick Play\' + Download season + Download Season + Which episodes do you want to download? + All Episodes + Unwatched Episodes