Merge pull request #6034 from Bnyro/master

feat: download popup that can be displayed over other apps
This commit is contained in:
Bnyro 2024-05-14 21:51:25 +02:00 committed by GitHub
commit 916b8a4e4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 167 additions and 72 deletions

View File

@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -23,6 +23,7 @@
android:name=".LibreTubeApp" android:name=".LibreTubeApp"
android:allowBackup="true" android:allowBackup="true"
android:banner="@mipmap/ic_launcher" android:banner="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -32,7 +33,6 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/StartupTheme" android:theme="@style/StartupTheme"
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
@ -79,6 +79,22 @@
</activity> </activity>
<activity
android:name=".ui.activities.DownloadActivity"
android:enabled="true"
android:exported="true"
android:label="@string/download"
android:launchMode="singleTop"
android:theme="@style/Theme.Material3.DayNight.Dialog">
<intent-filter android:label="@string/download">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity <activity
android:name=".ui.activities.OfflinePlayerActivity" android:name=".ui.activities.OfflinePlayerActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
@ -356,27 +372,27 @@
<service <service
android:name=".services.DownloadService" android:name=".services.DownloadService"
android:foregroundServiceType="dataSync"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="dataSync" />
<service <service
android:name=".services.PlaylistDownloadEnqueueService" android:name=".services.PlaylistDownloadEnqueueService"
android:foregroundServiceType="dataSync"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="dataSync" />
<service <service
android:name=".services.OnlinePlayerService" android:name=".services.OnlinePlayerService"
android:foregroundServiceType="mediaPlayback"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="mediaPlayback" />
<service <service
android:name=".services.OfflinePlayerService" android:name=".services.OfflinePlayerService"
android:foregroundServiceType="mediaPlayback"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="mediaPlayback" />
<receiver <receiver
android:name=".receivers.NotificationReceiver" android:name=".receivers.NotificationReceiver"

View File

@ -20,10 +20,10 @@ import com.github.libretube.obj.NewPipeSubscriptions
import com.github.libretube.obj.PipedImportPlaylist import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.obj.PipedPlaylistFile import com.github.libretube.obj.PipedPlaylistFile
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.util.TextUtils
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream import kotlinx.serialization.json.encodeToStream
import java.util.Date
import java.util.stream.Collectors import java.util.stream.Collectors
object ImportHelper { object ImportHelper {
@ -182,7 +182,7 @@ object ImportHelper {
val playlistName = lines[1].split(",").reversed().getOrNull(2) val playlistName = lines[1].split(",").reversed().getOrNull(2)
// the playlist name can be undefined in some cases, e.g. watch later lists // the playlist name can be undefined in some cases, e.g. watch later lists
playlist.name = playlistName ?: Date().toString() playlist.name = playlistName ?: TextUtils.defaultPlaylistName
// start directly at the beginning if header playlist info such as name is missing // start directly at the beginning if header playlist info such as name is missing
val startIndex = if (playlistName == null) { val startIndex = if (playlistName == null) {

View File

@ -14,6 +14,7 @@ import com.github.libretube.R
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.ui.sheets.IntentChooserSheet import com.github.libretube.ui.sheets.IntentChooserSheet
import com.github.libretube.util.TextUtils.toTimeInSeconds
object IntentHelper { object IntentHelper {
private fun getResolveIntent(link: String) = Intent(Intent.ACTION_VIEW) private fun getResolveIntent(link: String) = Intent(Intent.ACTION_VIEW)
@ -67,4 +68,36 @@ object IntentHelper {
Toast.makeText(context, R.string.no_player_found, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.no_player_found, Toast.LENGTH_SHORT).show()
} }
} }
/**
* Resolve the uri and return a bundle with the arguments
*/
fun resolveType(intent: Intent, uri: Uri) = with(intent) {
val lastSegment = uri.lastPathSegment
val secondLastSegment = uri.pathSegments.getOrNull(uri.pathSegments.size - 2)
when {
lastSegment == "results" -> {
putExtra(IntentData.query, uri.getQueryParameter("search_query"))
}
secondLastSegment == "channel" -> {
putExtra(IntentData.channelId, lastSegment)
}
secondLastSegment == "c" || secondLastSegment == "user" -> {
putExtra(IntentData.channelName, lastSegment)
}
lastSegment == "playlist" -> {
putExtra(IntentData.playlistId, uri.getQueryParameter("list"))
}
lastSegment == "watch_videos" -> {
putExtra(IntentData.playlistName, uri.getQueryParameter("title"))
val videoIds = uri.getQueryParameter("video_ids")?.split(",")
putExtra(IntentData.videoIds, videoIds?.toTypedArray())
}
else -> {
val id = if (lastSegment == "watch") uri.getQueryParameter("v") else lastSegment
putExtra(IntentData.videoId, id)
putExtra(IntentData.timeStamp, uri.getQueryParameter("t")?.toTimeInSeconds())
}
}
}
} }

View File

@ -112,6 +112,10 @@ object ThemeHelper {
if (pureThemeEnabled) activity.theme.applyStyle(R.style.Pure, true) if (pureThemeEnabled) activity.theme.applyStyle(R.style.Pure, true)
} }
fun applyDialogActivityTheme(activity: Activity) {
activity.theme.applyStyle(R.style.DialogActivity, true)
}
/** /**
* set the theme mode (light, dark, auto) * set the theme mode (light, dark, auto)
*/ */

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.net.toUri import androidx.core.net.toUri
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
@ -15,31 +16,23 @@ class AddToQueueActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val uri = intent.getStringExtra(Intent.EXTRA_TEXT)!!.toUri() val videoId = intent.getStringExtra(Intent.EXTRA_TEXT)
var videoId: String? = null ?.let { IntentHelper.resolveType(Intent(), it.toUri()) }
listOf("/shorts/", "/v/", "/embed/").forEach { ?.getStringExtra(IntentData.videoId)
if (uri.path!!.contains(it)) {
videoId = uri.path!!.replace(it, "") if (videoId != null) {
val newIntent = packageManager.getLaunchIntentForPackage(packageName)
// if playing a video currently, the video will be added to the queue
if (PlayingQueue.isNotEmpty()) {
PlayingQueue.insertByVideoId(videoId)
} else {
newIntent?.putExtra(IntentData.videoId, videoId)
} }
}
if ( startActivity(newIntent)
uri.path!!.contains("/watch") && uri.query != null
) {
videoId = uri.getQueryParameter("v")
} }
if (videoId == null) videoId = uri.path!!.replace("/", "")
val intent = packageManager.getLaunchIntentForPackage(packageName)
// if playing a video currently, the video will be added to the queue
if (PlayingQueue.isNotEmpty()) {
PlayingQueue.insertByVideoId(videoId!!)
} else {
intent?.putExtra(IntentData.videoId, videoId)
}
startActivity(intent)
finishAndRemoveTask() finishAndRemoveTask()
} }
} }

View File

@ -0,0 +1,44 @@
package com.github.libretube.ui.activities
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import com.github.libretube.constants.IntentData
import com.github.libretube.enums.PlaylistType
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.DownloadDialog
import com.github.libretube.ui.dialogs.DownloadPlaylistDialog
class DownloadActivity: BaseActivity() {
override val isDialogActivity: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intentData = intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
IntentHelper.resolveType(Intent(), it.toUri())
}
val videoId = intentData?.getStringExtra(IntentData.videoId)
val playlistId = intentData?.getStringExtra(IntentData.playlistId)
if (videoId != null) {
DownloadDialog().apply {
arguments = bundleOf(IntentData.videoId to videoId)
}.show(supportFragmentManager, null)
} else if (playlistId != null) {
DownloadPlaylistDialog().apply {
arguments = bundleOf(
IntentData.playlistId to playlistId,
IntentData.playlistType to PlaylistType.PUBLIC,
IntentData.playlistName to intent.getStringExtra(Intent.EXTRA_TITLE)
)
}.show(supportFragmentManager, null)
} else {
finishAndRemoveTask()
}
}
}

View File

@ -5,11 +5,10 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.core.net.toUri import androidx.core.net.toUri
import com.github.libretube.constants.IntentData
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.NavigationHelper import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.util.TextUtils.toTimeInSeconds
class RouterActivity : BaseActivity() { class RouterActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -25,42 +24,12 @@ class RouterActivity : BaseActivity() {
} }
} }
/**
* Resolve the uri and return a bundle with the arguments
*/
private fun Intent.resolveType(uri: Uri) = apply {
val lastSegment = uri.lastPathSegment
val secondLastSegment = uri.pathSegments.getOrNull(uri.pathSegments.size - 2)
when {
lastSegment == "results" -> {
putExtra(IntentData.query, uri.getQueryParameter("search_query"))
}
secondLastSegment == "channel" -> {
putExtra(IntentData.channelId, lastSegment)
}
secondLastSegment == "c" || secondLastSegment == "user" -> {
putExtra(IntentData.channelName, lastSegment)
}
lastSegment == "playlist" -> {
putExtra(IntentData.playlistId, uri.getQueryParameter("list"))
}
lastSegment == "watch_videos" -> {
putExtra(IntentData.playlistName, uri.getQueryParameter("title"))
val videoIds = uri.getQueryParameter("video_ids")?.split(",")
putExtra(IntentData.videoIds, videoIds?.toTypedArray())
}
else -> {
val id = if (lastSegment == "watch") uri.getQueryParameter("v") else lastSegment
putExtra(IntentData.videoId, id)
putExtra(IntentData.timeStamp, uri.getQueryParameter("t")?.toTimeInSeconds())
}
}
}
private fun handleSendText(uri: Uri) { private fun handleSendText(uri: Uri) {
Log.i(TAG(), uri.toString()) Log.i(TAG(), uri.toString())
val intent = packageManager.getLaunchIntentForPackage(packageName)!!.resolveType(uri) val intent = packageManager.getLaunchIntentForPackage(packageName)!!.let { intent ->
IntentHelper.resolveType(intent, uri)
}
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent) startActivity(intent)
finishAndRemoveTask() finishAndRemoveTask()

View File

@ -15,6 +15,8 @@ import com.github.libretube.helpers.WindowHelper
* Activity that applies the LibreTube theme and the in-app language * Activity that applies the LibreTube theme and the in-app language
*/ */
open class BaseActivity : AppCompatActivity() { open class BaseActivity : AppCompatActivity() {
open val isDialogActivity: Boolean = false
val screenOrientationPref by lazy { val screenOrientationPref by lazy {
val orientationPref = PreferenceHelper.getString( val orientationPref = PreferenceHelper.getString(
PreferenceKeys.ORIENTATION, PreferenceKeys.ORIENTATION,
@ -36,6 +38,7 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// set the app theme (e.g. Material You) // set the app theme (e.g. Material You)
ThemeHelper.updateTheme(this) ThemeHelper.updateTheme(this)
if (isDialogActivity) ThemeHelper.applyDialogActivityTheme(this)
// Set the navigation and statusBar color if SDK < 23 // Set the navigation and statusBar color if SDK < 23
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {

View File

@ -14,6 +14,7 @@ import com.github.libretube.extensions.getWhileDigit
import com.github.libretube.extensions.serializable import com.github.libretube.extensions.serializable
import com.github.libretube.helpers.LocaleHelper import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.services.PlaylistDownloadEnqueueService import com.github.libretube.services.PlaylistDownloadEnqueueService
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
class DownloadPlaylistDialog : DialogFragment() { class DownloadPlaylistDialog : DialogFragment() {
@ -25,7 +26,7 @@ class DownloadPlaylistDialog : DialogFragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
playlistId = requireArguments().getString(IntentData.playlistId)!! playlistId = requireArguments().getString(IntentData.playlistId)!!
playlistName = requireArguments().getString(IntentData.playlistName)!! playlistName = requireArguments().getString(IntentData.playlistName) ?: TextUtils.defaultPlaylistName
playlistType = requireArguments().serializable(IntentData.playlistType)!! playlistType = requireArguments().serializable(IntentData.playlistType)!!
} }

View File

@ -10,17 +10,17 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.obj.PipedImportPlaylist import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Date
class ImportTempPlaylistDialog : DialogFragment() { class ImportTempPlaylistDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = arguments?.getString(IntentData.playlistName) val title = arguments?.getString(IntentData.playlistName)
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?: Date().toString() ?: TextUtils.defaultPlaylistName
val videoIds = arguments?.getStringArray(IntentData.videoIds).orEmpty() val videoIds = arguments?.getStringArray(IntentData.videoIds).orEmpty()
return MaterialAlertDialogBuilder(requireContext()) return MaterialAlertDialogBuilder(requireContext())

View File

@ -14,6 +14,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.time.Duration import kotlin.time.Duration
import kotlinx.datetime.LocalDate as KotlinLocalDate import kotlinx.datetime.LocalDate as KotlinLocalDate
@ -33,6 +34,8 @@ object TextUtils {
*/ */
private val MEDIUM_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) private val MEDIUM_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
val defaultPlaylistName get() = Date().toString()
/** /**
* Localize the date from a date string, using the medium format. * Localize the date from a date string, using the medium format.
* @param date The date to parse * @param date The date to parse

View File

@ -262,4 +262,33 @@
</style> </style>
<style name="DialogActivity">
<item name="android:colorBackground">?attr/colorBackgroundFloating</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat</item>
<item name="android:windowTitleBackgroundStyle">@style/Base.DialogWindowTitleBackground.AppCompat</item>
<item name="android:windowBackground">@drawable/abc_dialog_material_background</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@style/Animation.AppCompat.Dialog</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
<item name="windowActionBar">false</item>
<item name="windowActionModeOverlay">true</item>
<item name="listPreferredItemPaddingLeft">24dip</item>
<item name="listPreferredItemPaddingRight">24dip</item>
<item name="android:listDivider">@null</item>
<item name="android:buttonBarStyle">@style/Widget.AppCompat.ButtonBar.AlertDialog</item>
<item name="android:borderlessButtonStyle">@style/Widget.AppCompat.Button.Borderless</item>
<item name="android:windowCloseOnTouchOutside">true</item>
</style>
</resources> </resources>