mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-15 06:40:30 +05:30
feat: prompt to play offline if video already downloaded
This commit is contained in:
parent
da8316ac3b
commit
8bf7f56ec8
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()) }
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
45
app/src/main/res/layout/dialog_play_offline.xml
Normal file
45
app/src/main/res/layout/dialog_play_offline.xml
Normal 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>
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user