feat: prompt to play offline if video already downloaded

This commit is contained in:
Angelo Reitano 2024-10-04 11:09:12 +02:00 committed by Bnyro
parent da8316ac3b
commit 8bf7f56ec8
9 changed files with 164 additions and 7 deletions

View File

@ -6,6 +6,7 @@ object IntentData {
const val id = "id" const val id = "id"
const val videoId = "videoId" const val videoId = "videoId"
const val videoIds = "videoIds" const val videoIds = "videoIds"
const val videoTitle = "videoTitle"
const val channelId = "channelId" const val channelId = "channelId"
const val channelName = "channelName" const val channelName = "channelName"
const val channelAvatar = "channelAvatar" const val channelAvatar = "channelAvatar"
@ -52,4 +53,6 @@ object IntentData {
const val downloadTab = "downloadTab" const val downloadTab = "downloadTab"
const val shuffle = "shuffle" const val shuffle = "shuffle"
const val noInternet = "noInternet" const val noInternet = "noInternet"
const val isPlayingOffline = "isPlayingOffline"
const val downloadInfo = "downloadInfo"
} }

View File

@ -21,7 +21,7 @@ interface DownloadDao {
@Transaction @Transaction
@Query("SELECT * FROM download WHERE videoId = :videoId") @Query("SELECT * FROM download WHERE videoId = :videoId")
suspend fun findById(videoId: String): DownloadWithItems suspend fun findById(videoId: String): DownloadWithItems?
@Query("SELECT videoId FROM downloadItem WHERE type = :fileType ORDER BY RANDOM() LIMIT 1") @Query("SELECT videoId FROM downloadItem WHERE type = :fileType ORDER BY RANDOM() LIMIT 1")
suspend fun getRandomVideoIdByFileType(fileType: FileType): String? suspend fun getRandomVideoIdByFileType(fileType: FileType): String?

View File

@ -12,6 +12,8 @@ import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.obj.DownloadItem import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.enums.FileType
import com.github.libretube.enums.PlaylistType import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainDispatcher
@ -139,4 +141,20 @@ object DownloadHelper {
} }
} }
} }
fun extractDownloadInfoText(context: Context, download: DownloadWithItems): List<String> {
val downloadInfo = mutableListOf<String>()
download.downloadItems.firstOrNull { it.type == FileType.VIDEO }?.let { videoItem ->
downloadInfo.add(context.getString(R.string.video) + ": ${videoItem.format} ${videoItem.quality}")
}
download.downloadItems.firstOrNull { it.type == FileType.AUDIO }?.let { audioItem ->
var infoString = ": ${audioItem.quality} ${audioItem.format})"
if (audioItem.language != null) infoString += " ${audioItem.language}"
downloadInfo.add(context.getString(R.string.audio) + infoString)
}
download.downloadItems.firstOrNull { it.type == FileType.SUBTITLE }?.let {
downloadInfo.add(context.getString(R.string.captions) + ": ${it.language}")
}
return downloadInfo
}
} }

View File

@ -69,7 +69,7 @@ class OfflinePlayerService : AbstractPlayerService() {
override suspend fun startPlaybackAndUpdateNotification() { override suspend fun startPlaybackAndUpdateNotification() {
val downloadWithItems = withContext(Dispatchers.IO) { val downloadWithItems = withContext(Dispatchers.IO) {
Database.downloadDao().findById(videoId) Database.downloadDao().findById(videoId)
} }!!
this.downloadWithItems = downloadWithItems this.downloadWithItems = downloadWithItems
onNewVideoStarted?.let { it(downloadWithItems.download.toStreamItem()) } onNewVideoStarted?.let { it(downloadWithItems.download.toStreamItem()) }

View File

@ -229,7 +229,7 @@ class OfflinePlayerActivity : BaseActivity() {
lifecycleScope.launch { lifecycleScope.launch {
val (downloadInfo, downloadItems, downloadChapters) = withContext(Dispatchers.IO) { val (downloadInfo, downloadItems, downloadChapters) = withContext(Dispatchers.IO) {
Database.downloadDao().findById(videoId) Database.downloadDao().findById(videoId)
} }!!
PlayingQueue.updateCurrent(downloadInfo.toStreamItem()) PlayingQueue.updateCurrent(downloadInfo.toStreamItem())
val chapters = downloadChapters.map(DownloadChapter::toChapterSegment) val chapters = downloadChapters.map(DownloadChapter::toChapterSegment)

View File

@ -0,0 +1,55 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DialogPlayOfflineBinding
import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class PlayOfflineDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogPlayOfflineBinding.inflate(layoutInflater)
val videoId = requireArguments().getString(IntentData.videoId)
binding.videoTitle.text = requireArguments().getString(IntentData.videoTitle)
val downloadInfo = requireArguments().getStringArray(IntentData.downloadInfo)
binding.downloadInfo.adapter =
ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, downloadInfo.orEmpty().map {
TextUtils.SEPARATOR + it
})
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.dialog_play_offline_title)
.setView(binding.root)
.setPositiveButton(R.string.yes) { _, _ ->
val intent = Intent(requireContext(), OfflinePlayerActivity::class.java)
.putExtra(IntentData.videoId, videoId)
requireContext().startActivity(intent)
setFragmentResult(
PLAY_OFFLINE_DIALOG_REQUEST_KEY,
bundleOf(IntentData.isPlayingOffline to true)
)
}
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
setFragmentResult(
PLAY_OFFLINE_DIALOG_REQUEST_KEY,
bundleOf(IntentData.isPlayingOffline to false)
)
}
.show()
}
companion object {
const val PLAY_OFFLINE_DIALOG_REQUEST_KEY = "play_offline_dialog_request_key"
}
}

View File

@ -65,6 +65,7 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentPlayerBinding import com.github.libretube.databinding.FragmentPlayerBinding
import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.PlayerEvent import com.github.libretube.enums.PlayerEvent
import com.github.libretube.enums.ShareObjectType import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.formatShort
@ -98,6 +99,7 @@ import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.AddToPlaylistDialog import com.github.libretube.ui.dialogs.AddToPlaylistDialog
import com.github.libretube.ui.dialogs.PlayOfflineDialog
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.animateDown import com.github.libretube.ui.extensions.animateDown
import com.github.libretube.ui.extensions.setupSubscriptionButton import com.github.libretube.ui.extensions.setupSubscriptionButton
@ -119,6 +121,7 @@ import com.github.libretube.util.TextUtils.toTimeInSeconds
import com.github.libretube.util.YoutubeHlsPlaylistParser import com.github.libretube.util.YoutubeHlsPlaylistParser
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.abs import kotlin.math.abs
@ -243,7 +246,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private val playerListener = object : Player.Listener { private val playerListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (PlayerHelper.pipEnabled || PictureInPictureCompat.isInPictureInPictureMode(mainActivity)) { if (PlayerHelper.pipEnabled || PictureInPictureCompat.isInPictureInPictureMode(
mainActivity
)
) {
PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams) PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams)
} }
@ -437,7 +443,37 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
binding.player.setCurrentChapterName() binding.player.setCurrentChapterName()
} }
val localDownloadVersion = runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().findById(videoId)
}
if (localDownloadVersion != null) {
childFragmentManager.setFragmentResultListener(
PlayOfflineDialog.PLAY_OFFLINE_DIALOG_REQUEST_KEY, viewLifecycleOwner
) { _, bundle ->
if (bundle.getBoolean(IntentData.isPlayingOffline)) {
// offline video playback started and thus the player fragment is no longer needed
killPlayerFragment()
} else {
playVideo() playVideo()
}
}
val downloadInfo = DownloadHelper.extractDownloadInfoText(
requireContext(),
localDownloadVersion
).toTypedArray()
PlayOfflineDialog().apply {
arguments = bundleOf(
IntentData.videoId to videoId,
IntentData.videoTitle to localDownloadVersion.download.title,
IntentData.downloadInfo to downloadInfo
)
}.show(childFragmentManager, null)
} else {
playVideo()
}
showBottomBar() showBottomBar()
} }

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/video_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="5dp"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Some random video title" />
<ListView
android:id="@+id/download_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/dialog_play_offline_body" />
</LinearLayout>

View File

@ -517,7 +517,8 @@
<string name="local_stream_extraction_summary">Directly fetch video playback information from YouTube without using Piped.</string> <string name="local_stream_extraction_summary">Directly fetch video playback information from YouTube without using Piped.</string>
<string name="local_ryd">Local Return Youtube Dislikes</string> <string name="local_ryd">Local Return Youtube Dislikes</string>
<string name="local_ryd_summary">Directly fetch dislike information from https://returnyoutubedislikeapi.com</string> <string name="local_ryd_summary">Directly fetch dislike information from https://returnyoutubedislikeapi.com</string>
<string name="view_count">%1$s views</string> <string name="dialog_play_offline_title">A local version of this video is available.</string>
<string name="dialog_play_offline_body">Would you like to play the video from the download folder?</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>
@ -563,5 +564,4 @@
<string name="also_clear_watch_positions">Also clear watch positions</string> <string name="also_clear_watch_positions">Also clear watch positions</string>
<string name="import_temp_playlist">Import temporary playlist?</string> <string name="import_temp_playlist">Import temporary playlist?</string>
<string name="import_temp_playlist_summary">Do you want to create a new playlist named \'%1$s\'? The playlist will contain %2$d videos.</string> <string name="import_temp_playlist_summary">Do you want to create a new playlist named \'%1$s\'? The playlist will contain %2$d videos.</string>
</resources> </resources>