other fixes

This commit is contained in:
zaednasr 2024-07-13 15:37:36 +02:00
parent 8e63219d73
commit fdb143310e
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
30 changed files with 549 additions and 522 deletions

View file

@ -6,6 +6,7 @@ import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.yausername.aria2c.Aria2c import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -44,6 +45,7 @@ class App : Application() {
e.printStackTrace() e.printStackTrace()
} }
} }
ThemeUtil.init(this)
} }
@Throws(YoutubeDLException::class) @Throws(YoutubeDLException::class)
private fun initLibraries() { private fun initLibraries() {

View file

@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern import java.util.regex.Pattern
class ResultRepository(private val resultDao: ResultDao, private val context: Context) { class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
private val tag: String = "ResultRepository"
val YTDLNIS_SEARCH = "YTDLNIS_SEARCH" val YTDLNIS_SEARCH = "YTDLNIS_SEARCH"
val allResults : Flow<List<ResultItem>> = resultDao.getResults() val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1) var itemCount = MutableStateFlow(-1)

View file

@ -1,6 +1,5 @@
package com.deniscerri.ytdl.database.viewmodel package com.deniscerri.ytdl.database.viewmodel
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
@ -9,12 +8,9 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Parcelable import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
@ -39,27 +35,24 @@ import com.deniscerri.ytdl.database.models.VideoPreferences
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.ui.downloadcard.FormatTuple
import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import okhttp3.internal.format
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -539,18 +532,17 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){ fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>, deleteExisting : Boolean = false) = viewModelScope.launch(Dispatchers.IO){
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing() repository.deleteProcessing()
processingItems.emit(true) processingItems.emit(true)
try { try {
itemIDs.forEachIndexed { idx, it -> itemIDs.forEach {
val item = repository.getItemByID(it) val item = repository.getItemByID(it)
if (processingItemsJob?.isCancelled == true) throw CancellationException() if (processingItemsJob?.isCancelled == true) throw CancellationException()
if (!deleteExisting) item.id = 0
item.id = 0
item.status = DownloadRepository.Status.Processing.toString() item.status = DownloadRepository.Status.Processing.toString()
repository.insert(item) repository.update(item)
} }
processingItems.emit(false) processingItems.emit(false)
} catch (e: Exception) { } catch (e: Exception) {
@ -1059,7 +1051,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingtoSavedStatus() dao.updateProcessingtoSavedStatus()
val id = System.currentTimeMillis().toInt() val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>() val workRequest = OneTimeWorkRequestBuilder<UpdateMultipleDownloadsFormatsWorker>()
.setInputData( .setInputData(
Data.Builder() Data.Builder()
.putLongArray("ids", ids.toLongArray()) .putLongArray("ids", ids.toLongArray())

View file

@ -308,7 +308,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
arguments?.remove("showDownloadsWithUpdatedFormats") arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") ?: return@launch val ids = arguments?.getLongArray("downloadIds") ?: return@launch
downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList()) downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList(), deleteExisting = true)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2) findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
} }

View file

@ -202,7 +202,8 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<ResultItem> = object : DiffUtil.ItemCallback<ResultItem>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<ResultItem> = object : DiffUtil.ItemCallback<ResultItem>() {
override fun areItemsTheSame(oldItem: ResultItem, newItem: ResultItem): Boolean { override fun areItemsTheSame(oldItem: ResultItem, newItem: ResultItem): Boolean {
return oldItem.id === newItem.id val ranged = arrayListOf(oldItem.id, newItem.id)
return ranged[0] == ranged[1]
} }
override fun areContentsTheSame(oldItem: ResultItem, newItem: ResultItem): Boolean { override fun areContentsTheSame(oldItem: ResultItem, newItem: ResultItem): Boolean {

View file

@ -108,8 +108,11 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
fragmentManager, fragmentManager,
lifecycle, lifecycle,
null, null,
currentDownloadItem currentDownloadItem,
isIncognito = incognito
) )
viewPager2.adapter = fragmentAdapter viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false viewPager2.isSaveFromParentEnabled = false
@ -182,7 +185,7 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
putString("last_used_download_type", putString("last_used_download_type",
listOf(Type.audio, Type.video, Type.command)[position].toString()) listOf(Type.audio, Type.video, Type.command)[position].toString())
} }
updateWhenSwitching() fragmentAdapter.updateWhenSwitching(viewPager2.currentItem)
} }
} }
}) })
@ -216,6 +219,7 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
} }
incognito = !incognito incognito = !incognito
fragmentAdapter.isIncognito = incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled) val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show()
} }
@ -224,54 +228,7 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
} }
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{ private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
val item = when(selectedTabPosition){ return fragmentAdapter.getDownloadItem(selectedTabPosition)
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem.apply { id = currentDownloadItem.id }
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.downloadItem.apply { id = currentDownloadItem.id }
}
else -> {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.downloadItem.apply { id = currentDownloadItem.id }
}
}
item.incognito = incognito
return item
}
private fun updateWhenSwitching(){
val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1
)
fragmentAdapter.setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
when(viewPager2.currentItem){
0 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
}
}
1 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(0).format)
}
}
2 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
else -> {}
}
} }

View file

@ -180,38 +180,32 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val data : MutableList<String?> = withContext(Dispatchers.IO){ val data = withContext(Dispatchers.IO) {
if (urls.isNullOrEmpty()) { if (urls.isNullOrEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url) infoUtil.getStreamingUrlAndChapters(item.url)
}else { }else{
urls.split("\n").toMutableList() Pair(urls.split("\n"), chapters)
} }
} }
if (data.isEmpty()) throw Exception("No Data found!") if (data.first.isEmpty()) throw Exception("No Data found!")
if (chapters!!.isEmpty() && urls!!.isBlank()){ if (chapters!!.isEmpty() && urls!!.isBlank()){
try{ chapters = data.second
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
chapters = Gson().fromJson(data.first().toString(), listType)
data.removeFirst()
}catch (ignored: Exception) {
data.removeFirst()
}
} }
if (data.isEmpty()) throw Exception("No Streaming URL found!") val urls = data.first
if (data.size == 2){ if (urls.size == 2){
val audioSource : MediaSource = val audioSource : MediaSource =
DefaultMediaSourceFactory(requireContext()) DefaultMediaSourceFactory(requireContext())
.createMediaSource(fromUri(Uri.parse(data[0]))) .createMediaSource(fromUri(Uri.parse(urls[0])))
val videoSource: MediaSource = val videoSource: MediaSource =
DefaultMediaSourceFactory(requireContext()) DefaultMediaSourceFactory(requireContext())
.createMediaSource(fromUri(Uri.parse(data[1]))) .createMediaSource(fromUri(Uri.parse(urls[1])))
player.setMediaSource(MergingMediaSource(videoSource, audioSource)) player.setMediaSource(MergingMediaSource(videoSource, audioSource))
}else{ }else{
player.addMediaItem(fromUri(Uri.parse(data[0]))) player.addMediaItem(fromUri(Uri.parse(urls[0])))
} }
player.addMediaItem(fromUri(Uri.parse(data[0])))
progress.visibility = View.GONE progress.visibility = View.GONE
populateSuggestedChapters() populateSuggestedChapters()
@ -221,6 +215,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
}catch (e: Exception){ }catch (e: Exception){
progress.visibility = View.GONE progress.visibility = View.GONE
frame.visibility = View.GONE frame.visibility = View.GONE
videoView.visibility = View.GONE
e.printStackTrace() e.printStackTrace()
} }
} }

View file

@ -239,10 +239,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
this.formats.removeAll(formats.toSet()) this.formats.removeAll(formats.toSet())
this.formats.addAll(allFormats.filter { !genericAudioFormats.contains(it) }) this.formats.addAll(allFormats.filter { !genericAudioFormats.contains(it) })
resultViewModel.update(this) resultViewModel.update(this)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(this)
}
} }
currentDownloadItem?.apply { currentDownloadItem?.apply {

View file

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -18,9 +17,7 @@ import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -190,18 +187,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
} }
//check if the item is coming from a text file
val isCommandOnly = (type == Type.command && !Patterns.WEB_URL.matcher(result.url).matches())
if (isCommandOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.alpha = 0.3f
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
(updateItem.parent as LinearLayout).visibility = View.GONE
}
//remove outdated player url of 1hr so it can refetch it in the cut player //remove outdated player url of 1hr so it can refetch it in the cut player
if (result.creationTime > System.currentTimeMillis() - 3600000) result.urls = "" if (result.creationTime > System.currentTimeMillis() - 3600000) result.urls = ""
@ -210,7 +195,8 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
fragmentManager, fragmentManager,
lifecycle, lifecycle,
result, result,
currentDownloadItem currentDownloadItem,
isIncognito = incognito
) )
viewPager2.adapter = fragmentAdapter viewPager2.adapter = fragmentAdapter
@ -239,6 +225,18 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}, 200) }, 200)
} }
} }
//check if the item is coming from a text file
val isCommandOnly = (type == Type.command && !Patterns.WEB_URL.matcher(result.url).matches())
if (isCommandOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.alpha = 0.3f
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
(updateItem.parent as LinearLayout).visibility = View.GONE
}
} }
sharedPreferences.edit(commit = true) { sharedPreferences.edit(commit = true) {
@ -294,7 +292,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
putString("last_used_download_type", putString("last_used_download_type",
listOf(Type.audio, Type.video, Type.command)[position].toString()) listOf(Type.audio, Type.video, Type.command)[position].toString())
} }
updateWhenSwitching() fragmentAdapter.updateWhenSwitching(viewPager2.currentItem)
} }
} }
}) })
@ -441,6 +439,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
} }
incognito = !incognito incognito = !incognito
fragmentAdapter.isIncognito = incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled) val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show()
} }
@ -498,42 +497,46 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
if (it){ if (it){
delay(500) delay(500)
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment (fragmentAdapter.fragments[0] as DownloadAudioFragment).apply {
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply { view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = true isVisible = true
isClickable = true isClickable = true
setOnClickListener { setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
resultViewModel.cancelUpdateFormatsItemData() resultViewModel.cancelUpdateFormatsItemData()
}
} }
} }
} }
} }
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment (fragmentAdapter.fragments[1] as DownloadVideoFragment).apply {
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply { view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = true isVisible = true
isClickable = true isClickable = true
setOnClickListener { setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
resultViewModel.cancelUpdateFormatsItemData() resultViewModel.cancelUpdateFormatsItemData()
}
} }
} }
} }
} }
}else{ }else{
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment (fragmentAdapter.fragments[0] as DownloadAudioFragment).apply {
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply { view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = false isVisible = false
isClickable = false isClickable = false
}
} }
} }
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment (fragmentAdapter.fragments[1] as DownloadVideoFragment).apply {
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply { view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = false isVisible = false
isClickable = false isClickable = false
}
} }
} }
} }
@ -546,43 +549,34 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
if (result == null) return@collectLatest if (result == null) return@collectLatest
kotlin.runCatching { kotlin.runCatching {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (result.isNotEmpty()){ if (result.size == 1 && result[0] != null) {
if (result.size == 1 && result[0] != null){ val res = result[0]!!
fragmentAdapter.setResultItem(result[0]!!) fragmentAdapter.setResultItem(res)
runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateUI(result[0])
}
runCatching { title.visibility = View.VISIBLE
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment subtitle.visibility = View.VISIBLE
f1.updateUI(result[0]) shimmerLoading.visibility = View.GONE
} shimmerLoadingSubtitle.visibility = View.GONE
shimmerLoading.stopShimmer()
shimmerLoadingSubtitle.stopShimmer()
title.visibility = View.VISIBLE val usingGenericFormatsOrEmpty = res.formats.isEmpty() || res.formats.any { it.format_note.contains("ytdlnisgeneric") }
subtitle.visibility = View.VISIBLE arguments?.putParcelable("result", res)
shimmerLoading.visibility = View.GONE if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false)){
shimmerLoadingSubtitle.visibility = View.GONE initUpdateFormats(res)
shimmerLoading.stopShimmer() }
shimmerLoadingSubtitle.stopShimmer()
val usingGenericFormatsOrEmpty = result[0]!!.formats.isEmpty() || result[0]!!.formats.any { it.format_note.contains("ytdlnisgeneric") } }else if (result.size > 1) {
fragmentAdapter.setResultItem(result[0]!!) //open multi download card instead
arguments?.putParcelable("result", result[0]!!) if (activity is ShareActivity){
if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false)){ findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_selectPlaylistItemsDialog, bundleOf(
initUpdateFormats(result[0]!!) Pair("resultIDs", result.map { it!!.id }.toLongArray()),
} ))
}else{ }else{
//open multi download card instead dismiss()
if (activity is ShareActivity){
findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_selectPlaylistItemsDialog, bundleOf(
Pair("resultIDs", result.map { it!!.id }.toLongArray()),
))
}else{
dismiss()
}
} }
} }
resultViewModel.updateResultData.emit(null) resultViewModel.updateResultData.emit(null)
} }
@ -606,19 +600,17 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment val f1 = fragmentAdapter.fragments[0] as DownloadAudioFragment
val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem) val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem)
resultItem.formats = formats resultItem.formats = formats
fragmentAdapter.setResultItem(resultItem) fragmentAdapter.setResultItem(resultItem)
f1.updateUI(resultItem)
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
} }
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment val f1 = fragmentAdapter.fragments[1] as DownloadVideoFragment
val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem) val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem)
resultItem.formats = formats resultItem.formats = formats
fragmentAdapter.setResultItem(resultItem) fragmentAdapter.setResultItem(resultItem)
f1.updateUI(resultItem)
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
} }
} }
@ -634,28 +626,12 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
} }
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem { private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem {
val item = when(selectedTabPosition){ return fragmentAdapter.getDownloadItem(selectedTabPosition)
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.downloadItem
}
else -> {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.downloadItem
}
}
item.incognito = incognito
return item
} }
private fun getAlsoAudioDownloadItem(finished: (it: DownloadItem) -> Unit) { private fun getAlsoAudioDownloadItem(finished: (it: DownloadItem) -> Unit) {
try { try {
val ff = (fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment) val ff = fragmentAdapter.fragments[0] as DownloadAudioFragment
ff.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first()) ff.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
finished(ff.downloadItem) finished(ff.downloadItem)
}catch (e: Exception){ }catch (e: Exception){
@ -680,37 +656,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
} }
} }
private fun updateWhenSwitching(){
val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1
)
fragmentAdapter.setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
when(viewPager2.currentItem){
0 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
}
}
1 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(0).format)
}
}
2 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
else -> {}
}
}
private fun initUpdateData() { private fun initUpdateData() {
kotlin.runCatching { kotlin.runCatching {
if (result.url.isBlank()) { if (result.url.isBlank()) {

View file

@ -12,33 +12,86 @@ class DownloadFragmentAdapter (
lifecycle : Lifecycle, lifecycle : Lifecycle,
private var result: ResultItem?, private var result: ResultItem?,
private var downloadItem: DownloadItem?, private var downloadItem: DownloadItem?,
private var nonSpecific: Boolean = false nonSpecific: Boolean = false,
var isIncognito: Boolean = false
) : FragmentStateAdapter(fragmentManager, lifecycle) { ) : FragmentStateAdapter(fragmentManager, lifecycle) {
fun setResultItem(res: ResultItem){ fun setResultItem(res: ResultItem){
result = res result = res
fragments.forEachIndexed { idx, it ->
when(idx) {
0 -> kotlin.runCatching { (it as DownloadAudioFragment).updateUI(res) }
1 -> kotlin.runCatching { (it as DownloadVideoFragment).updateUI(res) }
2 -> kotlin.runCatching { (it as DownloadCommandFragment).updateUI(res) }
}
}
} }
fun setDownloadItem(dd: DownloadItem){ private fun setTitleAuthor(t: String, a: String){
downloadItem = dd
}
fun setTitleAuthor(t: String, a: String){
result?.title = t result?.title = t
result?.author = a result?.author = a
downloadItem?.title = t downloadItem?.title = t
downloadItem?.author = a downloadItem?.author = a
} }
val fragments = listOf<Fragment>(
DownloadAudioFragment(result, downloadItem,"", nonSpecific),
DownloadVideoFragment(result, downloadItem,"", nonSpecific),
DownloadCommandFragment(result, downloadItem)
)
override fun getItemCount(): Int { override fun getItemCount(): Int {
return 3 return fragments.size
} }
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return when(position){ return fragments[position]
0 -> DownloadAudioFragment(result, downloadItem,"", nonSpecific) }
1 -> DownloadVideoFragment(result, downloadItem,"", nonSpecific)
else -> DownloadCommandFragment(result, downloadItem) fun updateWhenSwitching(currentViewPagerItem : Int) {
val prevDownloadItem = getDownloadItem(
if (currentViewPagerItem == 1) 0 else 1
)
setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
when(currentViewPagerItem){
0 -> {
kotlin.runCatching {
(fragments[0] as DownloadAudioFragment).apply {
updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
}
}
}
1 -> {
kotlin.runCatching {
(fragments[1] as DownloadVideoFragment).apply {
updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
updateSelectedAudioFormat(getDownloadItem(0).format)
}
}
}
2 -> {
kotlin.runCatching {
(fragments[2] as DownloadCommandFragment).apply {
updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
}
else -> {}
}
}
fun getDownloadItem(position: Int) : DownloadItem {
return when(position) {
0 -> (fragments[0] as DownloadAudioFragment).downloadItem
1 -> (fragments[1] as DownloadVideoFragment).downloadItem
else -> (fragments[2] as DownloadCommandFragment).downloadItem
}.apply {
incognito = isIncognito
} }
} }

View file

@ -257,10 +257,6 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
this.formats.removeAll(formats) this.formats.removeAll(formats)
this.formats.addAll(allFormats.filter { !genericVideoFormats.contains(it) }) this.formats.addAll(allFormats.filter { !genericVideoFormats.contains(it) })
resultViewModel.update(this) resultViewModel.update(this)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f1.updateUI(this)
}
} }
currentDownloadItem?.apply { currentDownloadItem?.apply {

View file

@ -220,6 +220,7 @@ class FormatSelectionBottomSheetDialog(
if (items.size == 1) { if (items.size == 1) {
kotlin.runCatching { kotlin.runCatching {
val res = infoUtil.getFormats(items.first()!!.url, currentFormatSource) val res = infoUtil.getFormats(items.first()!!.url, currentFormatSource)
if (!isActive) return@launch
res.filter { it.format_note != "storyboard" } res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) { chosenFormats = if (items.first()?.type == Type.audio) {
res.filter { it.format_note.contains("audio", ignoreCase = true) } res.filter { it.format_note.contains("audio", ignoreCase = true) }
@ -230,6 +231,8 @@ class FormatSelectionBottomSheetDialog(
formats.clear() formats.clear()
formats.addAll(res) formats.addAll(res)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
listener.onFormatsUpdated(res) listener.onFormatsUpdated(res)
} }

View file

@ -283,29 +283,28 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val data : MutableList<String?> = withContext(Dispatchers.IO){ val data = withContext(Dispatchers.IO) {
if (item.urls.isEmpty()) { if (item.urls.isEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url) infoUtil.getStreamingUrlAndChapters(item.url)
}else { }else{
item.urls.split("\n").toMutableList() Pair(item.urls.split("\n"), null)
} }
} }
if (data.size > 1) data.removeFirst() if (data.first.isEmpty()) throw Exception("No Data found!")
if (data.isEmpty()) throw Exception("No Streaming URL found!") val urls = data.first
if (data.size == 2){ if (urls.size == 2){
val audioSource : MediaSource = val audioSource : MediaSource =
DefaultMediaSourceFactory(requireContext()) DefaultMediaSourceFactory(requireContext())
.createMediaSource(MediaItem.fromUri(Uri.parse(data[0]))) .createMediaSource(MediaItem.fromUri(Uri.parse(urls[0])))
val videoSource: MediaSource = val videoSource: MediaSource =
DefaultMediaSourceFactory(requireContext()) DefaultMediaSourceFactory(requireContext())
.createMediaSource(MediaItem.fromUri(Uri.parse(data[1]))) .createMediaSource(MediaItem.fromUri(Uri.parse(urls[1])))
player.setMediaSource(MergingMediaSource(videoSource, audioSource)) player.setMediaSource(MergingMediaSource(videoSource, audioSource))
}else{ }else{
player.addMediaItem(MediaItem.fromUri(Uri.parse(data[0]))) player.addMediaItem(MediaItem.fromUri(Uri.parse(urls[0])))
} }
player.addMediaItem(MediaItem.fromUri(Uri.parse(data[0])))
player.prepare() player.prepare()
player.play() player.play()

View file

@ -126,6 +126,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pause.setOnClickListener { pause.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
pause.isEnabled = false
delay(1000)
workManager.cancelAllWorkByTag("download") workManager.cancelAllWorkByTag("download")
val activeDownloadsList = withContext(Dispatchers.IO){ val activeDownloadsList = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads() downloadViewModel.getActiveDownloads()
@ -140,16 +142,15 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
resume.isEnabled = false resume.isEnabled = false
resume.isVisible = true resume.isVisible = true
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
withContext(Dispatchers.Main){ resume.isEnabled = true
delay(1000)
resume.isEnabled = true
}
} }
} }
resume.setOnClickListener { resume.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
resume.isEnabled = false
delay(1000)
preferences.edit().putBoolean("paused_downloads", false).apply() preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false resume.isVisible = false
pause.isEnabled = false pause.isEnabled = false
@ -159,12 +160,9 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
downloadViewModel.startDownloadWorker(listOf()) downloadViewModel.startDownloadWorker(listOf())
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pause.isEnabled = true
} }
} }
withContext(Dispatchers.Main){
delay(1000)
pause.isEnabled = true
}
} }
} }
@ -204,9 +202,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
try { try {
progressBar?.setProgressCompat(event.progress, true) progressBar?.setProgressCompat(event.progress, true)
if (event.progress > 95) {
pause.isEnabled = false
}
outputText?.text = event.output outputText?.text = event.output
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
} }

View file

@ -48,6 +48,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.ui.adapter.HistoryAdapter import com.deniscerri.ytdl.ui.adapter.HistoryAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
@ -134,7 +135,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
requireActivity() requireActivity()
) )
recyclerView = view.findViewById(R.id.recyclerviewhistorys) recyclerView = view.findViewById(R.id.recyclerviewhistorys)
recyclerView.adapter = historyAdapter
recyclerView.enableFastScroll() recyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -165,6 +165,10 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyAdapter!!.submitList(it) historyAdapter!!.submitList(it)
historyList = it historyList = it
scrollToTop() scrollToTop()
if (recyclerView.adapter == null){
recyclerView.adapter = historyAdapter
}
} }
historyViewModel.sortOrder.observe(viewLifecycleOwner){ historyViewModel.sortOrder.observe(viewLifecycleOwner){
@ -552,7 +556,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
true true
} }
R.id.share -> { R.id.share -> {
UiUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten()) FileUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten())
clearCheckedItems() clearCheckedItems()
actionMode?.finish() actionMode?.finish()
true true
@ -677,7 +681,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
override fun onButtonClick(itemID: Long, isPresent: Boolean) { override fun onButtonClick(itemID: Long, isPresent: Boolean) {
if (isPresent){ if (isPresent){
UiUtil.shareFileIntent(requireContext(), historyList!!.first { it!!.id == itemID }!!.downloadPath) FileUtil.shareFileIntent(requireContext(), historyList!!.first { it!!.id == itemID }!!.downloadPath)
} }
} }
companion object { companion object {

View file

@ -161,7 +161,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
}else{ }else{
val snack = Snackbar.make(recyclerView, getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG) val snack = Snackbar.make(recyclerView, getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG)
snack.setAction(R.string.share) { snack.setAction(R.string.share) {
UiUtil.shareFileIntent(requireContext(), listOf(f.absolutePath)) FileUtil.shareFileIntent(requireContext(), listOf(f.absolutePath))
} }
snack.show() snack.show()
} }

View file

@ -89,7 +89,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
findPreference<Preference>("label_visibility")?.apply { findPreference<Preference>("label_visibility")?.apply {
isVisible = !resources.getBoolean(R.bool.uses_side_nav) isVisible = !resources.getBoolean(R.bool.uses_side_nav)
setOnPreferenceChangeListener { preference, newValue -> setOnPreferenceChangeListener { preference, newValue ->
restartApp() ThemeUtil.recreateMain()
true true
} }
} }
@ -162,7 +162,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
NavbarUtil.setNavBarItems(adapter.items, requireContext()) NavbarUtil.setNavBarItems(adapter.items, requireContext())
NavbarUtil.setStartFragment(adapter.selectedHomeTabId) NavbarUtil.setStartFragment(adapter.selectedHomeTabId)
restartApp() ThemeUtil.recreateMain()
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
@ -185,34 +185,18 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
theme!!.summary = getString(R.string.light) theme!!.summary = getString(R.string.light)
} }
} }
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) ThemeUtil.updateThemes()
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent)
requireActivity().finishAffinity()
true true
} }
accent!!.summary = accent!!.entry accent!!.summary = accent!!.entry
accent!!.onPreferenceChangeListener = accent!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) ThemeUtil.updateThemes()
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent)
requireActivity().finishAffinity()
true true
} }
highContrast!!.onPreferenceChangeListener = highContrast!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) ThemeUtil.updateThemes()
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent)
requireActivity().finishAffinity()
true true
} }

View file

@ -49,6 +49,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -220,7 +221,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json)) saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json))
val s = Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG) val s = Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG)
s.setAction(R.string.Open_File){ s.setAction(R.string.Open_File){
UiUtil.openFileIntent(requireActivity(), saveFile.absolutePath) FileUtil.openFileIntent(requireActivity(), saveFile.absolutePath)
} }
s.show() s.show()
} }

View file

@ -1,31 +1,50 @@
package com.deniscerri.ytdl.util package com.deniscerri.ytdl.util
import android.R.attr.mimeType
import android.app.Activity
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.ViewGroup
import android.view.Window
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.impl.utils.PreferenceUtils
import com.anggrayudi.storage.callback.FileCallback import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFolderTo import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.moveFileTo import com.anggrayudi.storage.file.moveFileTo
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okhttp3.internal.lowercase
import java.io.File import java.io.File
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
@ -327,4 +346,58 @@ object FileUtil {
return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}" return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}"
} }
fun openFileIntent(context: Context, downloadPath: String) {
val uri = FileProvider.getUriForFile(context, context.packageName + ".fileprovider", File(downloadPath))
println(uri)
if (uri == null){
Toast.makeText(context, "Error opening file!", Toast.LENGTH_SHORT).show()
}else{
val ext = downloadPath.substring(downloadPath.lastIndexOf(".") + 1).lowercase()
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
val intent = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, mime)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
context.startActivity(intent)
}
}
fun shareFileIntent(context: Context, paths: List<String>){
val uris : ArrayList<Uri> = arrayListOf()
paths.runCatching {
this.forEach {path ->
val uri = DocumentFile.fromSingleUri(context, Uri.parse(path)).run{
if (this?.exists() == true){
this.uri
}else if (File(path).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(path))
}else null
}
if (uri != null) uris.add(uri)
}
}
if (uris.isEmpty()){
Toast.makeText(context, "Error sharing files!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
type = if (uris.size == 1) uris[0].let { context.contentResolver.getType(it) } ?: "media/*" else "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}.run{
context.startActivity(Intent.createChooser(this, context.getString(R.string.share)))
}
}
}
} }

View file

@ -929,13 +929,22 @@ class InfoUtil(private val context: Context) {
fun getStreamingUrlAndChapters(url: String) : MutableList<String?> { fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
try { try {
val p = Pattern.compile("(^(https?)://(www.)?(music.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") if (url.isYoutubeURL()) {
val m = p.matcher(url) val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
throw Exception()
}else{
val item = createVideoFromPipedJSON(res, url)
if (item!!.urls.isBlank()) throw Exception()
val urls = item.urls.split(",")
val chapters = item.chapters
return Pair(urls, chapters)
}
if (m.find()){
return getStreamingUrlAndChaptersFromPIPED(url)
}else{ }else{
throw Exception() throw Exception()
} }
@ -943,7 +952,7 @@ class InfoUtil(private val context: Context) {
try { try {
val request = YoutubeDLRequest(url) val request = YoutubeDLRequest(url)
request.addOption("--get-url") request.addOption("--get-url")
request.addOption("--print", "%(chapters)s") request.addOption("--print", "%(.{urls,chapters})s")
request.addOption("--skip-download") request.addOption("--skip-download")
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
@ -964,8 +973,6 @@ class InfoUtil(private val context: Context) {
} }
} }
val proxy = sharedPreferences.getString("proxy", "") val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){ if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
@ -975,36 +982,41 @@ class InfoUtil(private val context: Context) {
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try { val json = JSONObject(youtubeDLResponse.out)
val lineSeparator = System.getProperty("line.separator") val urls = if (json.has("urls")) {
youtubeDLResponse.out.split(lineSeparator!!).toTypedArray() json.getString("urls").split("\n")
} catch (e: Exception) { }else{
arrayOf(youtubeDLResponse.out) listOf()
} }
return results.filter { it!!.isNotEmpty() }.toMutableList()
val chapters = if (json.has("chapters")) {
val arr = json.getJSONArray("chapters")
val list = mutableListOf<ChapterItem>()
for (i in 0 until arr.length()) {
list.add(
Gson().fromJson(arr.getJSONObject(i).toString(), ChapterItem::class.java)
)
}
list
}else{
listOf()
}
return Pair(urls, chapters)
} catch (e: Exception) { } catch (e: Exception) {
return mutableListOf() return Pair(listOf(), listOf())
} }
} }
} }
private fun getStreamingUrlAndChaptersFromPIPED(url: String) : MutableList<String?> {
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
throw Exception()
}else{
val item = createVideoFromPipedJSON(res, url)
if (item!!.urls.isBlank()) throw Exception()
val list = mutableListOf<String?>(Gson().toJson(item.chapters))
list.addAll(item.urls.split(","))
return list
}
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest { fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){ val useItemURL = sharedPreferences.getBoolean("use_itemurl_instead_playlisturl", false)
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || useItemURL){
YoutubeDLRequest(downloadItem.url) YoutubeDLRequest(downloadItem.url)
}else{ }else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply { YoutubeDLRequest(downloadItem.playlistURL!!).apply {
@ -1017,6 +1029,10 @@ class InfoUtil(private val context: Context) {
} }
} }
if (downloadItem.playlistIndex != null && useItemURL) {
request.addOption("--parse-metadata", " ${downloadItem.playlistIndex}: %(playlist_index)s")
}
val type = downloadItem.type val type = downloadItem.type
val downDir : File val downDir : File

View file

@ -11,6 +11,7 @@ import androidx.core.view.forEach
import androidx.core.view.get import androidx.core.view.get
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
@ -95,7 +96,7 @@ object NavbarUtil {
return navBarItems return navBarItems
} }
fun NavigationBarView.applyNavBarStyle(): Int { fun NavigationBarView.setLabelVisibility() {
val labelVisibilityMode = when ( val labelVisibilityMode = when (
settings.getString("label_visibility", "always") settings.getString("label_visibility", "always")
) { ) {
@ -105,6 +106,10 @@ object NavbarUtil {
else -> NavigationBarView.LABEL_VISIBILITY_AUTO else -> NavigationBarView.LABEL_VISIBILITY_AUTO
} }
this.labelVisibilityMode = labelVisibilityMode this.labelVisibilityMode = labelVisibilityMode
}
fun NavigationBarView.applyNavBarStyle(): Int {
setLabelVisibility()
val navBarItems = getNavBarItems(this.context) val navBarItems = getNavBarItems(this.context)

View file

@ -546,7 +546,7 @@ class NotificationUtil(var context: Context) {
) )
) )
.setContentText("") .setContentText("")
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_LOW)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setProgress(PROGRESS_MAX, PROGRESS_CURR, false) .setProgress(PROGRESS_MAX, PROGRESS_CURR, false)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)

View file

@ -1,8 +1,11 @@
package com.deniscerri.ytdl.util package com.deniscerri.ytdl.util
import android.app.Activity
import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle
import android.text.Spanned import android.text.Spanned
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
@ -11,12 +14,50 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
import androidx.test.runner.lifecycle.Stage
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
object ThemeUtil { object ThemeUtil {
private val activities = mutableListOf<Activity>()
fun init(app: Application) {
app.registerActivityLifecycleCallbacks(object: Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
activities.add(p0)
}
override fun onActivityStarted(p0: Activity) {
}
override fun onActivityResumed(p0: Activity) {
}
override fun onActivityPaused(p0: Activity) {
}
override fun onActivityStopped(p0: Activity) {
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
}
override fun onActivityDestroyed(p0: Activity) {
activities.remove(p0)
}
})
}
sealed class AppIcon( sealed class AppIcon(
@DrawableRes val iconResource: Int, @DrawableRes val iconResource: Int,
val activityAlias: String val activityAlias: String
@ -32,7 +73,18 @@ object ThemeUtil {
AppIcon.Dark AppIcon.Dark
) )
fun updateTheme(activity: AppCompatActivity) { fun recreateMain() {
activities.firstOrNull { it.javaClass == MainActivity::class.java }?.recreate()
}
fun updateThemes() {
activities.forEach {
updateTheme(it)
it.recreate()
}
}
fun updateTheme(activity: Activity) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
//update accent //update accent

View file

@ -389,112 +389,6 @@ object UiUtil {
} }
fun openFileIntent(context: Context, downloadPath: String) {
val uri = downloadPath.runCatching {
DocumentFile.fromSingleUri(context, Uri.parse(downloadPath)).run{
if (this?.exists() == true){
this.uri
}else if (File(this@runCatching).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(this@runCatching))
}else null
}
}.getOrNull()
if (uri == null){
Toast.makeText(context, "Error opening file!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_VIEW
data = uri
}.run{
context.startActivity(this)
}
}
}
private fun openMultipleFilesIntent(context: Activity, path: List<String>){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.filepathlist)
val list = bottomSheet.findViewById<LinearLayout>(R.id.filepath_list)
list?.apply {
path.forEach {path ->
val file = File(path)
val item = context.layoutInflater.inflate(R.layout.filepath_card, list, false)
item.apply {
findViewById<TextView>(R.id.file_name).text = file.nameWithoutExtension
findViewById<TextView>(R.id.duration).apply {
val duration = file.getMediaDuration(context)
isVisible = duration > 0
text = duration.toStringDuration(Locale.US)
}
findViewById<TextView>(R.id.filesize).text = FileUtil.convertFileSize(file.length())
findViewById<TextView>(R.id.extension).text = file.extension.uppercase()
if (!file.exists()){
isEnabled = false
alpha = 0.7f
}
isEnabled = file.exists()
setOnClickListener {
openFileIntent(context, path)
bottomSheet.dismiss()
}
}
list.addView(item)
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
context.windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun shareFileIntent(context: Context, paths: List<String>){
val uris : ArrayList<Uri> = arrayListOf()
paths.runCatching {
this.forEach {path ->
val uri = DocumentFile.fromSingleUri(context, Uri.parse(path)).run{
if (this?.exists() == true){
this.uri
}else if (File(path).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(path))
}else null
}
if (uri != null) uris.add(uri)
}
}
if (uris.isEmpty()){
Toast.makeText(context, "Error sharing files!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
type = if (uris.size == 1) uris[0].let { context.contentResolver.getType(it) } ?: "media/*" else "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}.run{
context.startActivity(Intent.createChooser(this, context.getString(R.string.share)))
}
}
}
fun showDatePickerOnly(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){ fun showDatePickerOnly(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){
val date = Calendar.getInstance() val date = Calendar.getInstance()
@ -816,7 +710,7 @@ object UiUtil {
} }
setOnClickListener { setOnClickListener {
shareFileIntent(context, item.downloadPath) FileUtil.shareFileIntent(context, item.downloadPath)
} }
} }
} }
@ -903,7 +797,7 @@ object UiUtil {
openFile!!.tag = item.id openFile!!.tag = item.id
openFile.setOnClickListener{ openFile.setOnClickListener{
if (item.downloadPath.size == 1) { if (item.downloadPath.size == 1) {
openFileIntent(context, item.downloadPath.first()) FileUtil.openFileIntent(context, item.downloadPath.first())
}else{ }else{
openMultipleFilesIntent(context, item.downloadPath) openMultipleFilesIntent(context, item.downloadPath)
} }
@ -2046,4 +1940,52 @@ object UiUtil {
} }
} }
} }
fun openMultipleFilesIntent(context: Activity, path: List<String>){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.filepathlist)
val list = bottomSheet.findViewById<LinearLayout>(R.id.filepath_list)
list?.apply {
path.forEach {path ->
val file = File(path)
val item = context.layoutInflater.inflate(R.layout.filepath_card, list, false)
item.apply {
findViewById<TextView>(R.id.file_name).text = file.nameWithoutExtension
findViewById<TextView>(R.id.duration).apply {
val duration = file.getMediaDuration(context)
isVisible = duration > 0
text = duration.toStringDuration(Locale.US)
}
findViewById<TextView>(R.id.filesize).text = FileUtil.convertFileSize(file.length())
findViewById<TextView>(R.id.extension).text = file.extension.uppercase()
if (!file.exists()){
isEnabled = false
alpha = 0.7f
}
isEnabled = file.exists()
setOnClickListener {
FileUtil.openFileIntent(context, path)
bottomSheet.dismiss()
}
}
list.addView(item)
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
context.windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
} }

View file

@ -137,7 +137,7 @@ class UpdateUtil(var context: Context) {
object : BroadcastReceiver() { object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) { override fun onReceive(context: Context?, intent: Intent) {
context?.unregisterReceiver(this) context?.unregisterReceiver(this)
UiUtil.openFileIntent(context!!, FileUtil.openFileIntent(context!!,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath +
File.separator + releaseVersion.name) File.separator + releaseVersion.name)
} }

View file

@ -13,7 +13,7 @@ import com.deniscerri.ytdl.util.NotificationUtil
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class UpdatePlaylistFormatsWorker( class UpdateMultipleDownloadsFormatsWorker(
private val context: Context, private val context: Context,
workerParams: WorkerParameters workerParams: WorkerParameters
) : Worker(context, workerParams) { ) : Worker(context, workerParams) {
@ -38,7 +38,7 @@ class UpdatePlaylistFormatsWorker(
ids.forEach { ids.forEach {
if (!isStopped){ if (!isStopped){
val d = dao.getDownloadById(it) val d = dao.getDownloadById(it)
val r = resDao.getResultByURL(d.url)!! val r = resDao.getResultByURL(d.url)
if (d.allFormats.isNotEmpty()){ if (d.allFormats.isNotEmpty()){
count++ count++
@ -50,18 +50,18 @@ class UpdatePlaylistFormatsWorker(
d.allFormats.addAll(infoUtil.getFormats(d.url)) d.allFormats.addAll(infoUtil.getFormats(d.url))
d.format = vm.getFormat(d.allFormats,d.type) d.format = vm.getFormat(d.allFormats,d.type)
r.formats.clear() r?.formats?.clear()
r.formats.addAll(d.allFormats) r?.formats?.addAll(d.allFormats)
runBlocking { runBlocking {
resDao.update(r) r?.apply { resDao.update(this) }
dao.update(d) dao.update(d)
} }
} }
count++ count++
notificationUtil.updateFormatUpdateNotification(workID, UpdatePlaylistFormatsWorker::class.java.name, count, ids.size) notificationUtil.updateFormatUpdateNotification(workID, UpdateMultipleDownloadsFormatsWorker::class.java.name, count, ids.size)
}else{ }else{
throw Exception() throw Exception()
} }
@ -79,8 +79,4 @@ class UpdatePlaylistFormatsWorker(
} }
} }
override fun onStopped() {
Log.e("asd", "Asd")
super.onStopped()
}
} }

View file

@ -37,103 +37,110 @@
app:useDefaultControls="true" app:useDefaultControls="true"
app:use_controller="false" /> app:use_controller="false" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:indeterminate="true"
android:translationZ="10dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pause"
android:visibility="gone"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_play_arrow_white"
android:layout_width="wrap_content"
android:indeterminate="true"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/rewind"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_rewind_white"
android:layout_marginHorizontal="20dp"
android:layout_width="wrap_content"
android:contentDescription="@string/restart"
android:indeterminate="true"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/mute"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:contentDescription="@string/remove_audio"
android:layout_height="wrap_content"
app:icon="@drawable/ic_music"
app:iconSize="30dp"
app:iconTint="?android:colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/forward"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_fast_forward_white"
android:layout_marginHorizontal="20dp"
android:layout_width="wrap_content"
android:contentDescription="@string/end"
android:indeterminate="true"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/durationText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#80000000"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:indeterminate="true"
android:translationZ="10dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pause"
android:visibility="gone"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_play_arrow_white"
android:layout_width="wrap_content"
android:indeterminate="true"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/rewind"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_rewind_white"
android:layout_marginHorizontal="20dp"
android:layout_width="wrap_content"
android:contentDescription="@string/restart"
android:indeterminate="true"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/mute"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:contentDescription="@string/remove_audio"
android:layout_height="wrap_content"
app:icon="@drawable/ic_music"
app:iconSize="30dp"
app:iconTint="?android:colorAccent"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/forward"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_fast_forward_white"
android:layout_marginHorizontal="20dp"
android:layout_width="wrap_content"
android:contentDescription="@string/end"
android:indeterminate="true"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/durationText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#80000000"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cut_section" android:id="@+id/cut_section"
@ -167,7 +174,6 @@
android:orientation="horizontal" android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/suggested_cuts" app:layout_constraintBottom_toTopOf="@id/suggested_cuts"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.533"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rangeSlider" app:layout_constraintTop_toBottomOf="@+id/rangeSlider"
app:layout_constraintVertical_bias="0.0"> app:layout_constraintVertical_bias="0.0">
@ -177,6 +183,7 @@
style="@style/Widget.Material3.TextInputLayout.FilledBox" style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="70dp"
android:hint="@string/start"> android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
@ -203,6 +210,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/end" android:hint="@string/end"
android:minWidth="70dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon"> app:layout_constraintStart_toEndOf="@+id/colon">

View file

@ -410,4 +410,6 @@
<string name="restore_info">Press \'Restore\' to merge the saved data with your current data.\nPress \'Reset\' to erase and only use the saved data in the file.</string> <string name="restore_info">Press \'Restore\' to merge the saved data with your current data.\nPress \'Reset\' to erase and only use the saved data in the file.</string>
<string name="use_alarm_manager">Use AlarmManager instead of WorkManager for Scheduling</string> <string name="use_alarm_manager">Use AlarmManager instead of WorkManager for Scheduling</string>
<string name="use_alarm_manager_summary">Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise</string> <string name="use_alarm_manager_summary">Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise</string>
<string name="use_item_url_not_playlist">Use Item URL instead of Playlist URL</string>
<string name="use_item_url_not_playlist_summary">Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled</string>
</resources> </resources>

View file

@ -99,6 +99,17 @@
app:defaultValue="" app:defaultValue=""
android:summary="@string/piped_instance_summary" android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" /> app:title="@string/piped_instance" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/baseline_format_quote_24"
app:key="use_itemurl_instead_playlisturl"
app:summary="@string/use_item_url_not_playlist_summary"
app:title="@string/use_item_url_not_playlist" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/misc"> <PreferenceCategory android:title="@string/misc">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<external-path name="external_files" path="."/> <external-path name="external_files" path="."/>
<root-path name="external_files" path="/storage/" /> <root-path name="external_files" path="." />
<cache-path <cache-path
name="cache" name="cache"
path="." /> path="." />