- added saved downloads in the settings backup
- made icon bigger in downloading notification
- fixed notification cancel and pausing not working
- fixed app not working in preferred home screen for downloads and more
- removed 000001 if the user has made only one cut
- removed log id from downloads if the user deleted the log
- if the user had multiple preferred audio formats the app would choose the last one and not the first
- added ability to combine preferred audio formats by writing like 140+251,250. If the download item finds both first audio formats it will merge them together, otherwise get one of them that is available
- fixed app file transfer not working. (had to revert to hidden cache folder as downloads folder wasnt good for some phones)
- fixed app saving as mkv even though mp4 is set as container
- added shortcuts button to the commands tab so you can drop shortcuts to the current command.
- added syntax highlighting in commands tab textbox
- fixed tiktok videos not saving properly
- added ability to combine all possible combinations of preferred video formats, audio formats, codex and container.

The app will consider all preferred video ids. For each of them it will consider preferred vcodec and extension. At the same time for each of those elements it will consider all preferred audio commands and/or audio merge.
If the format is a generic quality like 1080p or 720p it will also consider that in the long format query. Also the app will add all combos of video + audio only. And all combos of all video + best audio. All video ids alone and best as final fallback.
If the user chooses a proper format, the app will not be this descriptive. Preferred video id and audio id combos will only apply on a quick download or best quality generic download
This commit is contained in:
deniscerri 2023-08-26 20:48:24 +02:00
parent 5b3bb3c2ed
commit df2dee4c83
No known key found for this signature in database
GPG key ID: 95C43D517D830350
38 changed files with 665 additions and 430 deletions

View file

@ -10,7 +10,7 @@ def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 6
def versionPatch = 5 def versionPatch = 5
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 2 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""
if (versionBuild > 0){ if (versionBuild > 0){

View file

@ -120,14 +120,14 @@ class MainActivity : BaseActivity() {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
// val graph = navController.navInflater.inflate(R.navigation.nav_graph) val graph = navController.navInflater.inflate(R.navigation.nav_graph)
// graph.setStartDestination(R.id.homeFragment) graph.setStartDestination(R.id.homeFragment)
// when(sharedPreferences.getString("start_destination", "")) { when(sharedPreferences.getString("start_destination", "")) {
// "History" -> graph.setStartDestination(R.id.historyFragment) "History" -> graph.setStartDestination(R.id.historyFragment)
// "More" -> if (navigationView is NavigationBarView) graph.setStartDestination(R.id.moreFragment) "More" -> if (navigationView is NavigationBarView) graph.setStartDestination(R.id.moreFragment)
// } }
//
// navController.graph = graph navController.graph = graph
if (navigationView is NavigationBarView){ if (navigationView is NavigationBarView){
(navigationView as NavigationBarView).setupWithNavController(navController) (navigationView as NavigationBarView).setupWithNavController(navController)

View file

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.google.android.material.card.MaterialCardView
class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CookieItem?, CookieAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CookieItem?, CookieAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
@ -23,10 +24,10 @@ class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val item: ConstraintLayout val item: MaterialCardView
init { init {
item = itemView.findViewById(R.id.command_template_item_constraint) item = itemView.findViewById(R.id.command_card)
} }
} }

View file

@ -54,7 +54,6 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
card.isVisible = item != null
if (item == null) return if (item == null) return
card.tag = item.id.toString() card.tag = item.id.toString()

View file

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.google.android.material.card.MaterialCardView
class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CommandTemplate?, TemplatesAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CommandTemplate?, TemplatesAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
@ -24,10 +25,10 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val item: ConstraintLayout val item: MaterialCardView
init { init {
item = itemView.findViewById(R.id.command_template_item_constraint) item = itemView.findViewById(R.id.command_card)
} }
} }

View file

@ -4,7 +4,6 @@ import androidx.paging.PagingSource
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@ -23,6 +22,9 @@ interface DownloadDao {
fun getDownloadsCountByStatus(status : List<String>) : Flow<Int> fun getDownloadsCountByStatus(status : List<String>) : Flow<Int>
@Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem> fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'")
@ -64,6 +66,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : PagingSource<Int, DownloadItem> fun getSavedDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem fun getDownloadById(id: Long) : DownloadItem
@ -119,6 +124,12 @@ interface DownloadDao {
@Upsert @Upsert
suspend fun update(item: DownloadItem) suspend fun update(item: DownloadItem)
@Query("UPDATE downloads SET logID=null")
fun removeAllLogID()
@Query("UPDATE downloads SET logID=null WHERE logID=:logID")
fun removeLogID(logID: Long)
@Upsert @Upsert
fun updateMultiple(items :List<DownloadItem>) fun updateMultiple(items :List<DownloadItem>)

View file

@ -22,6 +22,13 @@ interface LogDao {
@Query("DELETE FROM logs WHERE id=:itemId") @Query("DELETE FROM logs WHERE id=:itemId")
suspend fun delete(itemId: Long) suspend fun delete(itemId: Long)
@Transaction
suspend fun deleteItems(list: List<LogItem>){
list.forEach{
delete(it.id)
}
}
@Update(onConflict = OnConflictStrategy.REPLACE) @Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: LogItem) suspend fun update(item: LogItem)

View file

@ -6,7 +6,7 @@ data class VideoPreferences (
var splitByChapters: Boolean = false, var splitByChapters: Boolean = false,
var sponsorBlockFilters: ArrayList<String> = arrayListOf(), var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false, var writeSubs: Boolean = false,
var subsLanguages: String = "all", var subsLanguages: String = "en.*,.*-orig",
var audioFormatIDs : ArrayList<String> = arrayListOf(), var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false var removeAudio: Boolean = false
) )

View file

@ -63,7 +63,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
} }
fun getActiveDownloads() : List<DownloadItem> { fun getActiveDownloads() : List<DownloadItem> {
return downloadDao.getActiveDownloadsList() return downloadDao.getActiveAndPausedDownloadsList()
} }
fun getActiveAndQueuedDownloads() : List<DownloadItem> { fun getActiveAndQueuedDownloads() : List<DownloadItem> {
@ -110,4 +110,12 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.unPauseActiveAndQueued() downloadDao.unPauseActiveAndQueued()
} }
fun removeLogID(logID: Long){
downloadDao.removeLogID(logID)
}
fun removeAllLogID(){
downloadDao.removeAllLogID()
}
} }

View file

@ -77,6 +77,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private var audioContainer: String? private var audioContainer: String?
private var videoContainer: String? private var videoContainer: String?
private var videoCodec: String? private var videoCodec: String?
private val dao: DownloadDao
enum class Type { enum class Type {
audio, video, command audio, video, command
@ -84,7 +85,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
init { init {
dbManager = DBManager.getInstance(application) dbManager = DBManager.getInstance(application)
val dao = dbManager.downloadDao dao = dbManager.downloadDao
repository = DownloadRepository(dao) repository = DownloadRepository(dao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
@ -157,13 +158,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
) )
}else{ }else{
Format( Format(
audioFormatIDPreference.first(), audioFormatIDPreference.first().split("+").first(),
audioContainer!!, audioContainer!!,
"", "",
"", "",
"", "",
0, 0,
audioFormatIDPreference.first() audioFormatIDPreference.first().split("+").first()
) )
} }
@ -220,8 +221,24 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val audioPreferences = AudioPreferences(embedThumb, false, ArrayList(sponsorblock!!)) val audioPreferences = AudioPreferences(embedThumb, false, ArrayList(sponsorblock!!))
val hasPreferredAudioFormats = resultItem.formats.filter { audioFormatIDPreference.contains(it.format_id) } val preferredAudioFormats = arrayListOf<String>()
val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, audioFormatIDs = if (hasPreferredAudioFormats.isEmpty()) arrayListOf() else arrayListOf(hasPreferredAudioFormats.map { it.format_id }.first())) for (f in resultItem.formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { resultItem.formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
val videoPreferences = VideoPreferences(
embedSubs,
addChapters, false,
ArrayList(sponsorblock),
saveSubs,
audioFormatIDs = preferredAudioFormats
)
return DownloadItem(0, return DownloadItem(0,
resultItem.url, resultItem.url,
@ -368,10 +385,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return cloneFormat ( return cloneFormat (
try { try {
try{ try{
formats.filter { it.format_note.contains("audio", ignoreCase = true)}.first { formats.filter { it.format_note.contains("audio", ignoreCase = true)}.sortedBy { it.format_id }
audioFormatIDPreference.contains(it.format_id) || .firstOrNull { audioFormatIDPreference.indexOfFirst { a -> a.contains(it.format_id) } >= 0 || it.container == audioContainer} ?: throw Exception()
it.container == audioContainer
}
}catch (e: Exception){ }catch (e: Exception){
formats.last { it.format_note.contains("audio", ignoreCase = true) } formats.last { it.format_note.contains("audio", ignoreCase = true) }
} }
@ -384,14 +399,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.video -> { Type.video -> {
return cloneFormat( return cloneFormat(
try { try {
val theFormats = formats.filter { !it.format_note.contains("audio", true) }.ifEmpty { defaultVideoFormats } val theFormats = formats.filter { !it.format_note.contains("audio", true) }.sortedBy { it.format_id }.ifEmpty { defaultVideoFormats }
when (videoQualityPreference) { when (videoQualityPreference) {
"worst" -> { "worst" -> {
theFormats.first() theFormats.first()
} }
else /*best*/ -> { else /*best*/ -> {
val requirements: MutableList<(Format) -> Boolean> = mutableListOf() val requirements: MutableList<(Format) -> Boolean> = mutableListOf()
requirements.add { it: Format -> formatIDPreference.contains(it.format_id) } requirements.add { it: Format -> formatIDPreference.sorted().contains(it.format_id) }
requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)} requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)}
requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) } requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) }
requirements.add { it: Format -> it.vcodec.startsWith(videoCodec!!, true) } requirements.add { it: Format -> it.vcodec.startsWith(videoCodec!!, true) }
@ -505,6 +520,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getErroredDownloads() return repository.getErroredDownloads()
} }
fun getSaved() : List<DownloadItem> {
return dao.getSavedDownloadsList()
}
private fun cloneFormat(item: Format) : Format { private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java) val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java) return Gson().fromJson(string, Format::class.java)
@ -567,9 +586,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
} }
if (queuedItems.isNotEmpty()){ startDownloadWorker(queuedItems)
startDownloadWorker(queuedItems)
}
} }
private suspend fun startDownloadWorker(queuedItems: List<DownloadItem>) { private suspend fun startDownloadWorker(queuedItems: List<DownloadItem>) {

View file

@ -7,17 +7,19 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.repository.LogRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LogViewModel(private val application: Application) : AndroidViewModel(application) { class LogViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: LogRepository private val repository: LogRepository
private val downloadRepository: DownloadRepository
val items: LiveData<List<LogItem>> val items: LiveData<List<LogItem>>
init { init {
val dao = DBManager.getInstance(application).logDao repository = LogRepository(DBManager.getInstance(application).logDao)
repository = LogRepository(dao) downloadRepository = DownloadRepository(DBManager.getInstance(application).downloadDao)
items = repository.items.asLiveData() items = repository.items.asLiveData()
} }
@ -36,10 +38,12 @@ class LogViewModel(private val application: Application) : AndroidViewModel(appl
fun delete(item: LogItem) = viewModelScope.launch(Dispatchers.IO) { fun delete(item: LogItem) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item) repository.delete(item)
downloadRepository.removeLogID(item.id)
} }
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll() repository.deleteAll()
downloadRepository.removeAllLogID()
} }
fun update(newLine: String, id: Long) = viewModelScope.launch(Dispatchers.IO) { fun update(newLine: String, id: Long) = viewModelScope.launch(Dispatchers.IO) {

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
@ -15,10 +16,10 @@ import kotlinx.coroutines.runBlocking
class CancelDownloadNotificationReceiver : BroadcastReceiver() { class CancelDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) { override fun onReceive(c: Context, intent: Intent) {
val message = intent.getStringExtra("cancel") val id = intent.getIntExtra("itemID", 0)
val id = intent.getIntExtra("workID", 0) if (id > 0) {
if (message != null) {
runCatching { runCatching {
Log.e("aa", id.toString())
val notificationUtil = NotificationUtil(c) val notificationUtil = NotificationUtil(c)
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)

View file

@ -0,0 +1,27 @@
package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class CancelWorkReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
val id = intent.getStringExtra("workID")
if (!id.isNullOrBlank()) {
runCatching {
WorkManager.getInstance(c).cancelAllWorkByTag(id)
}
}
}
}

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
@ -13,14 +14,14 @@ import kotlinx.coroutines.launch
class PauseDownloadNotificationReceiver : BroadcastReceiver() { class PauseDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) { override fun onReceive(c: Context, intent: Intent) {
val id = intent.getIntExtra("workID", 0) val id = intent.getIntExtra("itemID", 0)
if (id != 0) { if (id != 0) {
runCatching { runCatching {
val title = intent.getStringExtra("title") val title = intent.getStringExtra("title")
val notificationUtil = NotificationUtil(c) val notificationUtil = NotificationUtil(c)
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) notificationUtil.createResumeDownload(id, title)
val dbManager = DBManager.getInstance(c) val dbManager = DBManager.getInstance(c)
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
runCatching { runCatching {

View file

@ -63,7 +63,7 @@ class ResumeActivity : BaseActivity() {
} }
private fun handleIntents(intent: Intent) { private fun handleIntents(intent: Intent) {
val id = intent.getIntExtra("workID", 0) val id = intent.getIntExtra("itemID", 0)
if (id != 0) { if (id != 0) {
try { try {
val loadingBottomSheet = BottomSheetDialog(this) val loadingBottomSheet = BottomSheetDialog(this)
@ -71,13 +71,12 @@ class ResumeActivity : BaseActivity() {
loadingBottomSheet.setContentView(R.layout.please_wait_bottom_sheet) loadingBottomSheet.setContentView(R.layout.please_wait_bottom_sheet)
loadingBottomSheet.setOnShowListener { loadingBottomSheet.setOnShowListener {
NotificationUtil(this).cancelDownloadNotification(NotificationUtil.DOWNLOAD_RESUME_NOTIFICATION_ID) NotificationUtil(this).cancelDownloadNotification(NotificationUtil.DOWNLOAD_RESUME_NOTIFICATION_ID + id)
lifecycleScope.launch { lifecycleScope.launch {
val downloadViewModel = ViewModelProvider(this@ResumeActivity)[DownloadViewModel::class.java] val downloadViewModel = ViewModelProvider(this@ResumeActivity)[DownloadViewModel::class.java]
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
val downloadItem = downloadViewModel.getItemByID(id.toLong()) downloadViewModel.reQueueDownloadItems(listOf(id.toLong()))
downloadViewModel.queueDownloads(listOf(downloadItem)) finishAffinity()
finishAffinity()
} }
} }

View file

@ -23,6 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -427,11 +428,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private suspend fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){ private suspend fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){
searchSuggestionsLinearLayout!!.visibility = GONE
searchHistoryLinearLayout!!.visibility = GONE
searchSuggestionsLinearLayout!!.removeAllViews() searchSuggestionsLinearLayout!!.removeAllViews()
searchHistoryLinearLayout!!.removeAllViews() searchHistoryLinearLayout!!.removeAllViews()
searchSuggestionsLinearLayout!!.visibility = GONE
searchHistoryLinearLayout!!.visibility = GONE
linkYouCopied!!.visibility = GONE linkYouCopied!!.visibility = GONE
if (searchView!!.editText.text.isEmpty()){ if (searchView!!.editText.text.isEmpty()){

View file

@ -24,6 +24,7 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -95,29 +96,8 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba
view.findViewById<View>(R.id.current).visibility = View.GONE view.findViewById<View>(R.id.current).visibility = View.GONE
} }
text.enableTextHighlight()
//init syntax highlighter currentText.enableTextHighlight()
val highlight = Highlight()
val highlightWatcher = HighlightTextWatcher()
val schemes = listOf(
ColorScheme(Pattern.compile("([\"'])(?:\\\\1|.)*?\\1"), Color.parseColor("#FC8500")),
ColorScheme(Pattern.compile("yt-dlp"), Color.parseColor("#00FF00")),
ColorScheme(Pattern.compile("(https?://(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?://(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"), Color.parseColor("#b5942f")),
ColorScheme(Pattern.compile("\\d+(\\.\\d)?%"), Color.parseColor("#43a564"))
)
highlight.addScheme(
*schemes.map { it }.toTypedArray()
)
highlightWatcher.addScheme(
*schemes.map { it }.toTypedArray()
)
highlight.setSpan(currentText)
currentText.addTextChangedListener(highlightWatcher)
highlight.setSpan(text)
text.addTextChangedListener(highlightWatcher)
text?.setText(item?.extraCommands) text?.setText(item?.extraCommands)
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

View file

@ -28,7 +28,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.chip.Chip import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight
import com.deniscerri.ytdlnis.util.UiUtil.populateCommandCard
import com.google.android.material.card.MaterialCardView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -61,6 +63,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
} }
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
@ -79,7 +82,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
} }
val chosenCommandView = view.findViewById<TextInputLayout>(R.id.content) val chosenCommandView = view.findViewById<TextInputLayout>(R.id.content)
chosenCommandView.editText!!.setText(downloadItem.format.format_note) chosenCommandView.editText?.setText(downloadItem.format.format_note)
chosenCommandView.editText?.enableTextHighlight()
chosenCommandView.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_delete_all) chosenCommandView.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_delete_all)
chosenCommandView.editText!!.addTextChangedListener(object : TextWatcher { chosenCommandView.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
@ -113,25 +117,29 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
} }
chosenCommandView.editText!!.enableScrollText() chosenCommandView.editText!!.enableScrollText()
val commandTemplates = view.findViewById<TextInputLayout>(R.id.template) val commandTemplateCard = view.findViewById<MaterialCardView>(R.id.command_card)
val autoCompleteTextView = populateCommandCard(commandTemplateCard, templates.first())
requireView().findViewById<AutoCompleteTextView>(R.id.template_textview)
populateCommandTemplates(templates, autoCompleteTextView)
(commandTemplates!!.editText as AutoCompleteTextView?)!!.onItemClickListener = commandTemplateCard.setOnClickListener {
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long -> lifecycleScope.launch {
chosenCommandView.editText!!.setText(templates[index].content) UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel){
downloadItem.format = Format( chosenCommandView.editText!!.setText(it.content)
templates[index].title, populateCommandCard(commandTemplateCard, it)
"", downloadItem.format = Format(
"", it.title,
"", "",
"", "",
0, "",
templates[index].content "",
) 0,
it.content
)
}
} }
}
saveDir = view.findViewById(R.id.outputPath) saveDir = view.findViewById(R.id.outputPath)
saveDir.editText!!.setText( saveDir.editText!!.setText(
FileUtil.formatPath(downloadItem.downloadPath) FileUtil.formatPath(downloadItem.downloadPath)
@ -151,9 +159,14 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace
)) ))
val shortcutCount = withContext(Dispatchers.IO){
commandTemplateViewModel.getTotalShortcutNumber()
}
UiUtil.configureCommand( UiUtil.configureCommand(
view, view,
1, 1,
shortcutCount,
newTemplateClicked = { newTemplateClicked = {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it) templates.add(it)
@ -167,16 +180,24 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
0, 0,
it.content it.content
) )
populateCommandTemplates(templates, autoCompleteTextView) populateCommandCard(commandTemplateCard, it)
} }
}, },
editSelectedClicked = { editSelectedClicked =
var current = templates.find { it.title == autoCompleteTextView.text.toString() } {
if (current == null) current = CommandTemplate(0, "", chosenCommandView.editText!!.text.toString(), false) var current = templates.find { it.id == commandTemplateCard.findViewById<android.widget.TextView>(
R.id.id).toString().toLong() }
if (current == null) current =
CommandTemplate(
0,
"",
chosenCommandView.editText!!.text.toString(),
false
)
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it) templates.add(it)
chosenCommandView.editText!!.setText(it.content) chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format( downloadItem.format = com.deniscerri.ytdlnis.database.models.Format(
it.title, it.title,
"", "",
"", "",
@ -186,29 +207,22 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
it.content it.content
) )
} }
},
shortcutClicked = {
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) { sh ->
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $sh")
downloadItem.format.format_note += " $sh"
}
} }
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }
private fun populateCommandTemplates(templates: List<CommandTemplate>, autoCompleteTextView: AutoCompleteTextView?){
val templateTitles = templates.map {it.title}
autoCompleteTextView?.setAdapter(
ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
templateTitles
)
)
if (templateTitles.isNotEmpty()) {
autoCompleteTextView!!.setText(downloadItem.format.format_id, false)
}
}
private var commandPathResultLauncher = registerForActivityResult( private var commandPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()

View file

@ -75,34 +75,27 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pauseResume.setOnClickListener { pauseResume.setOnClickListener {
if (pauseResume.text == requireContext().getString(R.string.pause)){ if (pauseResume.text == requireContext().getString(R.string.pause)){
lifecycleScope.launch { lifecycleScope.launch {
workManager.cancelAllWorkByTag("download")
pauseResume.isEnabled = false pauseResume.isEnabled = false
val queued = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
queued.forEach { // pause active
it.status = DownloadRepository.Status.QueuedPaused.toString() withContext(Dispatchers.IO){
runBlocking {
downloadViewModel.updateDownload(it)
}
}
queued.forEach {
workManager.cancelAllWorkByTag(it.id.toString())
}
val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads() downloadViewModel.getActiveDownloads()
} }.forEach {
active.forEach {
cancelItem(it.id.toInt()) cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString() it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
} }
// pause queued
withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it)
}
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true pauseResume.isEnabled = true
} }
@ -116,17 +109,22 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
val toQueue = active.filter { it.status == DownloadRepository.Status.Paused.toString() }.toMutableList() val toQueue = active.filter { it.status == DownloadRepository.Status.Paused.toString() }.toMutableList()
toQueue.forEach { toQueue.forEach {
it.status = DownloadRepository.Status.Active.toString() it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
} }
runBlocking {
downloadViewModel.queueDownloads(listOf())
}
val queuedItems = withContext(Dispatchers.IO){ val queuedItems = withContext(Dispatchers.IO){
downloadViewModel.getQueued() downloadViewModel.getQueued()
} }
queuedItems.map { it.status = DownloadRepository.Status.Queued.toString() } queuedItems.map {
toQueue.addAll(queuedItems) it.status = DownloadRepository.Status.Queued.toString()
runBlocking { downloadViewModel.updateDownload(it)
downloadViewModel.queueDownloads(toQueue)
} }
pauseResume.isEnabled = true pauseResume.isEnabled = true
} }
} }
@ -220,20 +218,15 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
activeDownloads.notifyItemChanged(position) activeDownloads.notifyItemChanged(position)
val count = withContext(Dispatchers.IO){ runBlocking {
downloadViewModel.getActiveDownloads() downloadViewModel.queueDownloads(listOf(item))
}.count()
val queue = if (count > 1) listOf(item)
else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item)
list
} }
runBlocking { withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(queue) downloadViewModel.getQueued().filter { it.status != DownloadRepository.Status.Queued.toString() }.forEach {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
} }
} }
} }
@ -255,7 +248,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private fun cancelItem(id: Int){ private fun cancelItem(id: Int){
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)
} }

View file

@ -35,6 +35,7 @@ import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight
import com.deniscerri.ytdlnis.work.TerminalDownloadWorker import com.deniscerri.ytdlnis.work.TerminalDownloadWorker
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
@ -230,27 +231,8 @@ class TerminalActivity : BaseActivity() {
} }
initMenu() initMenu()
//init syntax highlighter input?.enableTextHighlight()
val highlight = Highlight() output?.enableTextHighlight()
val highlightWatcher = HighlightTextWatcher()
val schemes = listOf(
ColorScheme(Pattern.compile("([\"'])(?:\\\\1|.)*?\\1"), Color.parseColor("#FC8500")),
ColorScheme(Pattern.compile("yt-dlp"), Color.parseColor("#00FF00")),
ColorScheme(Pattern.compile("(https?://(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?://(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"), Color.parseColor("#b5942f")),
ColorScheme(Pattern.compile("\\d+(\\.\\d)?%"), Color.parseColor("#43a564"))
)
highlight.addScheme(
*schemes.map { it }.toTypedArray()
)
highlightWatcher.addScheme(
*schemes.map { it }.toTypedArray()
)
highlight.setSpan(output)
highlight.setSpan(input)
input?.addTextChangedListener(highlightWatcher)
output?.addTextChangedListener(highlightWatcher)
} }
private fun initMenu() { private fun initMenu() {

View file

@ -113,14 +113,14 @@ class FolderSettingsFragment : BaseSettingsFragment() {
audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
audioFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" audioFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
var cacheSize = File(FileUtil.getCachePath()).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } var cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
clearCache!!.onPreferenceClickListener = clearCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
if (activeDownloadCount == 0){ if (activeDownloadCount == 0){
File(FileUtil.getCachePath()).deleteRecursively() File(FileUtil.getCachePath(requireContext())).deleteRecursively()
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show() Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
cacheSize = File(FileUtil.getCachePath()).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
}else{ }else{
Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show() Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()

View file

@ -157,6 +157,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
"queued" -> json.add("queued", backupQueuedDownloads() ) "queued" -> json.add("queued", backupQueuedDownloads() )
"cancelled" -> json.add("cancelled", backupCancelledDownloads() ) "cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"errored" -> json.add("errored", backupErroredDownloads() ) "errored" -> json.add("errored", backupErroredDownloads() )
"saved" -> json.add("saved", backupSavedDownloads() )
"cookies" -> json.add("cookies", backupCookies() ) "cookies" -> json.add("cookies", backupCookies() )
"templates" -> json.add("templates", backupCommandTemplates() ) "templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() ) "shortcuts" -> json.add("shortcuts", backupShortcuts() )
@ -293,6 +294,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
return JsonArray() return JsonArray()
} }
private suspend fun backupSavedDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getSaved()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCookies() : JsonArray { private suspend fun backupCookies() : JsonArray {
runCatching { runCatching {
val items = withContext(Dispatchers.IO) { val items = withContext(Dispatchers.IO) {
@ -484,6 +499,28 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
} }
} }
//erorred downloads restore
if(json.has("saved")){
val items = json.getAsJsonArray("saved")
val saved = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
saved.add(item)
}
saved.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(saved.isNotEmpty()){
finalMessage.append("${getString(R.string.saved)}: ${saved.count()}\n")
}
}
//cookies restore //cookies restore
if(json.has("cookies")){ if(json.has("cookies")){
val items = json.getAsJsonArray("cookies") val items = json.getAsJsonArray("cookies")

View file

@ -5,13 +5,9 @@ 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.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.callback.FileCallback import com.anggrayudi.storage.callback.FileCallback
@ -19,17 +15,15 @@ import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFileTo import com.anggrayudi.storage.file.copyFileTo
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.getBasePath
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
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 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.CopyOption
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.text.DecimalFormat import java.text.DecimalFormat
@ -91,8 +85,7 @@ object FileUtil {
if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach
var destFile: DocumentFile var destFile: DocumentFile
try { try {
if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)|(live_chat)".toRegex())){ if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(.*.part-Frag)|(.*.live_chat)".toRegex())){
it.delete()
return@forEach return@forEach
} }
@ -269,8 +262,17 @@ object FileUtil {
return listOf(context.getString(R.string.unfound_file)) return listOf(context.getString(R.string.unfound_file))
} }
fun getCachePath() : String { fun getCachePath(context: Context) : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/.CACHE" return context.cacheDir.absolutePath + "/downloads/"
}
fun deleteConfigFiles(request: YoutubeDLRequest) {
request.getArguments("--config")?.forEach {
if (it != null) File(it).delete()
}
request.getArguments("--config-locations")?.forEach {
if (it != null) File(it).delete()
}
} }
fun getDefaultAudioPath() : String{ fun getDefaultAudioPath() : String{

View file

@ -13,7 +13,6 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -546,6 +545,7 @@ class InfoUtil(private val context: Context) {
fun getFromYTDL(query: String): ArrayList<ResultItem?> { fun getFromYTDL(query: String): ArrayList<ResultItem?> {
items = ArrayList() items = ArrayList()
val webpage_urls = mutableListOf<String>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch") val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
try { try {
val request : YoutubeDLRequest val request : YoutubeDLRequest
@ -610,12 +610,6 @@ class InfoUtil(private val context: Context) {
} }
} }
val url = if (jsonObject.has("url")){
jsonObject.getString("url")
}else{
jsonObject.getString("webpage_url")
}
var thumb: String? = "" var thumb: String? = ""
if (jsonObject.has("thumbnail")) { if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail") thumb = jsonObject.getString("thumbnail")
@ -652,6 +646,12 @@ class InfoUtil(private val context: Context) {
urls = urlList.joinToString("\n") urls = urlList.joinToString("\n")
} }
val url = if (jsonObject.has("url")){
jsonObject.getString("url")
}else{
jsonObject.getString("webpage_url")
}
items.add(ResultItem(0, items.add(ResultItem(0,
url, url,
title, title,
@ -665,6 +665,10 @@ class InfoUtil(private val context: Context) {
chapters chapters
) )
) )
webpage_urls.add(jsonObject.getString("webpage_url"))
}
if (items.size == 1){
items[0]?.url = webpage_urls.first()
} }
} catch (e: Exception) { } catch (e: Exception) {
Looper.prepare().run { Looper.prepare().run {
@ -929,7 +933,7 @@ class InfoUtil(private val context: Context) {
} }
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val cacheDir = FileUtil.getCachePath() val cacheDir = FileUtil.getCachePath(context)
val tempFileDir = File(cacheDir, downloadItem.id.toString()) val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete() tempFileDir.delete()
tempFileDir.mkdirs() tempFileDir.mkdirs()
@ -1009,7 +1013,10 @@ class InfoUtil(private val context: Context) {
request.addOption("--force-keyframes-at-cuts") request.addOption("--force-keyframes-at-cuts")
} }
} }
downloadItem.customFileNameTemplate += " %(section_title|)s %(autonumber)s" downloadItem.customFileNameTemplate += " %(section_title|)s "
if (downloadItem.downloadSections.split(";").size > 1){
downloadItem.customFileNameTemplate += "%(autonumber)s"
}
request.addOption("--output-na-placeholder", " ") request.addOption("--output-na-placeholder", " ")
} }
@ -1018,7 +1025,7 @@ class InfoUtil(private val context: Context) {
} }
if (downloadItem.extraCommands.isNotBlank()){ if (downloadItem.extraCommands.isNotBlank()){
val conf = File(tempFileDir.absolutePath + "/${System.currentTimeMillis()}.txt") val conf = File(FileUtil.getCachePath(context) + "/${System.currentTimeMillis()}.txt")
conf.createNewFile() conf.createNewFile()
conf.writeText(downloadItem.extraCommands) conf.writeText(downloadItem.extraCommands)
request.addOption( request.addOption(
@ -1097,7 +1104,7 @@ class InfoUtil(private val context: Context) {
if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg") if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg")
if (sharedPreferences.getBoolean("crop_thumbnail", true)){ if (sharedPreferences.getBoolean("crop_thumbnail", true)){
try { try {
val config = File(tempFileDir.absolutePath + "/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt") val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData) config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
@ -1129,39 +1136,18 @@ class InfoUtil(private val context: Context) {
} }
if (downloadItem.videoPreferences.embedSubs) { if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs") request.addOption("--embed-subs")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages) request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
} }
val defaultFormats = context.resources.getStringArray(R.array.video_formats)
if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()) request.addOption("--audio-multistreams") var cont = ""
var videoFormatID = downloadItem.format.format_id
Log.e(DownloadWorker.TAG, videoFormatID)
var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best"
if (videoFormatID.isNotEmpty()) {
if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo"
else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst"
else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
"$videoFormatID+$audioIds/$videoFormatID/best"
}else{
"$videoFormatID+bestaudio/$videoFormatID/best"
}
}
Log.e(DownloadWorker.TAG, formatArgument)
request.addOption("-f", formatArgument)
if (downloadItem.format.container.equals("mpeg_4", true)) downloadItem.format.container = "mp4"
val outputContainer = downloadItem.container val outputContainer = downloadItem.container
if( if(
outputContainer.isNotEmpty() && outputContainer.isNotEmpty() &&
outputContainer != "Default" && outputContainer != "Default" &&
outputContainer != context.getString(R.string.defaultValue) && outputContainer != context.getString(R.string.defaultValue) &&
supportedContainers.contains(outputContainer) && supportedContainers.contains(outputContainer)
!outputContainer.equals(downloadItem.format.container, true)
){ ){
cont = outputContainer
request.addOption("--merge-output-format", outputContainer.lowercase()) request.addOption("--merge-output-format", outputContainer.lowercase())
if (outputContainer != "webm") { if (outputContainer != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
@ -1171,6 +1157,103 @@ class InfoUtil(private val context: Context) {
} }
} }
//format logic
val defaultFormats = context.resources.getStringArray(R.array.video_formats)
var usingGenericFormat = false
var videof = downloadItem.format.format_id
if (videof == context.resources.getString(R.string.best_quality) || videof == "best") {
videof = "bestvideo"
usingGenericFormat = true
}else if (videof == context.resources.getString(R.string.worst_quality) || videof == "worst") {
videof = "worst"
}else if (defaultFormats.contains(videof)) {
videof = "bestvideo[height<="+videof.substring(0, videof.length -1)+"]"
}
val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString()
.split(",")
.filter { it.isNotEmpty() }
.ifEmpty { listOf(videof) }
var audiof = "bestaudio"
if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()){
audiof = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
}
if (downloadItem.videoPreferences.removeAudio) audiof = ""
val preferredAudioFormatIDs = sharedPreferences.getString("format_id_audio", "")
.toString()
.split(",")
.filter { it.isNotEmpty() }
.ifEmpty { listOf(audiof) }
val preferredVideoFormats = if (usingGenericFormat) preferredFormatIDs else listOf(videof)
val preferredAudioFormats = if (usingGenericFormat && downloadItem.videoPreferences.audioFormatIDs.isEmpty()) {
preferredAudioFormatIDs
} else listOf(audiof)
if (preferredAudioFormats.any{it.contains("+")}){
request.addOption("--audio-multistreams")
}
val preferredCodec = sharedPreferences.getString("video_codec", "")
val extPref = if (cont.isNotBlank()) "[ext=$cont]" else ""
val vCodecPref = if (preferredCodec!!.isNotBlank()) "[vcodec^=$preferredCodec]" else ""
val f = StringBuilder()
//build format with extension and vcodec
preferredVideoFormats.forEach { v ->
preferredAudioFormats.forEach { a ->
val aa = if (a.isNotBlank()) "+$a" else ""
f.append("$v$extPref$vCodecPref$aa/")
}
}
//build format with vcodec
if (extPref.isNotBlank()){
preferredVideoFormats.forEach { v ->
preferredAudioFormats.forEach { a ->
val aa = if (a.isNotBlank()) "+$a" else ""
f.append("$videof$vCodecPref$aa/")
}
}
}
if (vCodecPref.isNotBlank()){
preferredVideoFormats.forEach { v ->
//build format with vcodec
preferredAudioFormats.forEach {a ->
val aa = if (a.isNotBlank()) "+$a" else ""
f.append("$v$aa/")
}
}
}
//build format with best audio
preferredVideoFormats.forEach { v ->
if(!f.contains("$v+bestaudio/")){
f.append("$v+bestaudio/")
}
}
//build formats with standalone video
preferredVideoFormats.forEach { v ->
if(!f.contains("/$v/")){
f.append("$v/")
}
}
if(!f.endsWith("best/")){
//last fallback
f.append("best")
}
request.addOption("-f", f.toString().replace("/$".toRegex(), ""))
if (downloadItem.videoPreferences.writeSubs){ if (downloadItem.videoPreferences.writeSubs){
val subFormat = sharedPreferences.getString("sub_format", "srt") val subFormat = sharedPreferences.getString("sub_format", "srt")
@ -1179,7 +1262,7 @@ class InfoUtil(private val context: Context) {
request.addOption("--sub-format", "${subFormat}/best") request.addOption("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", "srt") request.addOption("--convert-subtitles", "srt")
if (!downloadItem.videoPreferences.embedSubs) { if (!downloadItem.videoPreferences.embedSubs) {
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages) request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
} }
} }
@ -1202,7 +1285,7 @@ class InfoUtil(private val context: Context) {
DownloadViewModel.Type.command -> { DownloadViewModel.Type.command -> {
request.addOption( request.addOption(
"--config-locations", "--config-locations",
File(FileUtil.getCachePath() + "/config[${downloadItem.id}].txt").apply { File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply {
writeText(downloadItem.format.format_note) writeText(downloadItem.format.format_note)
}.absolutePath }.absolutePath
) )

View file

@ -15,6 +15,7 @@ import androidx.core.content.FileProvider
import androidx.navigation.NavDeepLinkBuilder import androidx.navigation.NavDeepLinkBuilder
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver
import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.ResumeActivity import com.deniscerri.ytdlnis.receiver.ResumeActivity
import com.deniscerri.ytdlnis.receiver.ShareFileActivity import com.deniscerri.ytdlnis.receiver.ShareFileActivity
@ -81,7 +82,7 @@ class NotificationUtil(var context: Context) {
return notificationBuilder return notificationBuilder
.setContentTitle(context.getString(R.string.downloading)) .setContentTitle(context.getString(R.string.downloading))
.setOngoing(true) .setOngoing(true)
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, context.resources,
@ -98,30 +99,11 @@ class NotificationUtil(var context: Context) {
fun createDownloadServiceNotification( fun createDownloadServiceNotification(
pendingIntent: PendingIntent?, pendingIntent: PendingIntent?,
title: String?, title: String?,
workID: Int, itemID: Int,
channel: String
): Notification { ): Notification {
val notificationBuilder = getBuilder(channel) val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID)
val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java)
pauseIntent.putExtra("workID", workID)
pauseIntent.putExtra("title", title)
val pauseNotificationPendingIntent = PendingIntent.getBroadcast(
context,
workID,
pauseIntent,
PendingIntent.FLAG_IMMUTABLE
)
val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java)
cancelIntent.putExtra("cancel", "")
cancelIntent.putExtra("workID", workID)
val cancelNotificationPendingIntent = PendingIntent.getBroadcast(
context,
workID,
cancelIntent,
PendingIntent.FLAG_IMMUTABLE
)
return notificationBuilder return notificationBuilder
.setContentTitle(title) .setContentTitle(title)
.setOngoing(true) .setOngoing(true)
@ -140,13 +122,11 @@ class NotificationUtil(var context: Context) {
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions() .clearActions()
.addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent)
.addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent)
.build() .build()
} }
fun createResumeDownload(workID: Int, title: String?, channel: String){ fun createResumeDownload(itemID: Int, title: String?){
val notificationBuilder = getBuilder(channel) val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID)
notificationBuilder notificationBuilder
.setContentTitle(title) .setContentTitle(title)
@ -162,16 +142,16 @@ class NotificationUtil(var context: Context) {
.clearActions() .clearActions()
val intent = Intent(context, ResumeActivity::class.java) val intent = Intent(context, ResumeActivity::class.java)
intent.putExtra("workID", workID) intent.putExtra("itemID", itemID)
val resumeNotificationPendingIntent = PendingIntent.getActivity( val resumeNotificationPendingIntent = PendingIntent.getActivity(
context, context,
workID, itemID,
intent, intent,
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
notificationBuilder.addAction(0, context.getString(R.string.resume), resumeNotificationPendingIntent) notificationBuilder.addAction(0, context.getString(R.string.resume), resumeNotificationPendingIntent)
notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID, notificationBuilder.build()) notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID + itemID, notificationBuilder.build())
} }
fun createUpdatingItemNotification(channel: String){ fun createUpdatingItemNotification(channel: String){
@ -315,10 +295,33 @@ class NotificationUtil(var context: Context) {
var contentText = "" var contentText = ""
if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" + "\n" if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" + "\n"
contentText += desc.replace("\\[.*?\\] ".toRegex(), "") contentText += desc.replace("\\[.*?\\] ".toRegex(), "")
val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java)
pauseIntent.putExtra("itemID", id)
pauseIntent.putExtra("title", title)
val pauseNotificationPendingIntent = PendingIntent.getBroadcast(
context,
id,
pauseIntent,
PendingIntent.FLAG_IMMUTABLE
)
val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java)
cancelIntent.putExtra("itemID", id)
val cancelNotificationPendingIntent = PendingIntent.getBroadcast(
context,
id,
cancelIntent,
PendingIntent.FLAG_IMMUTABLE
)
try { try {
notificationBuilder.setProgress(100, progress, false) notificationBuilder.setProgress(100, progress, false)
.setContentTitle(title) .setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()
.addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent)
.addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent)
notificationManager.notify(id, notificationBuilder.build()) notificationManager.notify(id, notificationBuilder.build())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -388,8 +391,7 @@ class NotificationUtil(var context: Context) {
fun createFormatsUpdateNotification(workID: Int): Notification { fun createFormatsUpdateNotification(workID: Int): Notification {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java) val cancelIntent = Intent(context, CancelWorkReceiver::class.java)
cancelIntent.putExtra("cancel", "")
cancelIntent.putExtra("workID", workID) cancelIntent.putExtra("workID", workID)
val cancelNotificationPendingIntent = PendingIntent.getBroadcast( val cancelNotificationPendingIntent = PendingIntent.getBroadcast(
context, context,
@ -473,7 +475,7 @@ class NotificationUtil(var context: Context) {
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2" const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"
const val DOWNLOAD_FINISHED_CHANNEL_ID = "3" const val DOWNLOAD_FINISHED_CHANNEL_ID = "3"
const val DOWNLOAD_FINISHED_NOTIFICATION_ID = 3 const val DOWNLOAD_FINISHED_NOTIFICATION_ID = 3
const val DOWNLOAD_RESUME_NOTIFICATION_ID = 4 const val DOWNLOAD_RESUME_NOTIFICATION_ID = 40000
const val DOWNLOAD_UPDATING_NOTIFICATION_ID = 5 const val DOWNLOAD_UPDATING_NOTIFICATION_ID = 5
const val FORMAT_UPDATING_NOTIFICATION_ID = 6 const val FORMAT_UPDATING_NOTIFICATION_ID = 6
const val FORMAT_UPDATING_FINISHED_NOTIFICATION_ID = 7 const val FORMAT_UPDATING_FINISHED_NOTIFICATION_ID = 7

View file

@ -7,6 +7,7 @@ import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape import android.graphics.drawable.shapes.OvalShape
import android.net.Uri import android.net.Uri
@ -30,7 +31,6 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -62,13 +62,18 @@ import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern
object UiUtil { object UiUtil {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -85,7 +90,7 @@ object UiUtil {
if (audioFormats.isNullOrEmpty()){ if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase() formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase()
}else{ }else{
val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]" val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { "(${it.format_id}) ${it.format_note}" }}]"
formatCard.findViewById<TextView>(R.id.format_note).text = title formatCard.findViewById<TextView>(R.id.format_note).text = title
} }
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}" formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}"
@ -109,6 +114,12 @@ object UiUtil {
} }
fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){
card.findViewById<TextView>(R.id.title).text = item.title
card.findViewById<TextView>(R.id.content).text = item.content
card.findViewById<TextView>(R.id.id).text = item.id.toString()
}
fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){ fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){
val bottomSheet = BottomSheetDialog(context) val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -658,7 +669,7 @@ object UiUtil {
linearLayout!!.removeAllViews() linearLayout!!.removeAllViews()
list.forEach {template -> list.forEach {template ->
val item = activity.layoutInflater.inflate(R.layout.command_template_item, linearLayout, false) as ConstraintLayout val item = activity.layoutInflater.inflate(R.layout.command_template_item, linearLayout, false) as MaterialCardView
item.findViewById<TextView>(R.id.title).text = template.title item.findViewById<TextView>(R.id.title).text = template.title
item.findViewById<TextView>(R.id.content).text = template.content item.findViewById<TextView>(R.id.content).text = template.content
item.setOnClickListener { item.setOnClickListener {
@ -719,8 +730,12 @@ object UiUtil {
extraCommandsClicked: () -> Unit extraCommandsClicked: () -> Unit
){ ){
val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles) val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles)
val saveSubtitles = view.findViewById<Chip>(R.id.save_subtitles)
val subtitleLanguages = view.findViewById<Chip>(R.id.subtitle_languages)
embedSubs!!.isChecked = items.all { it.videoPreferences.embedSubs } embedSubs!!.isChecked = items.all { it.videoPreferences.embedSubs }
embedSubs.setOnClickListener { embedSubs.setOnClickListener {
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
embedSubsClicked(embedSubs.isChecked) embedSubsClicked(embedSubs.isChecked)
} }
@ -878,8 +893,6 @@ object UiUtil {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
} }
val saveSubtitles = view.findViewById<Chip>(R.id.save_subtitles)
val subtitleLanguages = view.findViewById<Chip>(R.id.subtitle_languages)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -890,11 +903,11 @@ object UiUtil {
} }
saveSubtitles.setOnCheckedChangeListener { _, _ -> saveSubtitles.setOnCheckedChangeListener { _, _ ->
if (saveSubtitles.isChecked) subtitleLanguages.visibility = View.VISIBLE subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
else subtitleLanguages.visibility = View.GONE
saveSubtitlesClicked(saveSubtitles.isChecked) saveSubtitlesClicked(saveSubtitles.isChecked)
} }
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
subtitleLanguages.setOnClickListener { subtitleLanguages.setOnClickListener {
subtitleLanguagesClicked() subtitleLanguagesClicked()
} }
@ -1074,8 +1087,10 @@ object UiUtil {
fun configureCommand( fun configureCommand(
view: View, view: View,
size: Int, size: Int,
shortcutCount: Int,
newTemplateClicked: () -> Unit, newTemplateClicked: () -> Unit,
editSelectedClicked: () -> Unit, editSelectedClicked: () -> Unit,
shortcutClicked: suspend () -> Unit,
){ ){
val newTemplate : Chip = view.findViewById(R.id.newTemplate) val newTemplate : Chip = view.findViewById(R.id.newTemplate)
newTemplate.setOnClickListener { newTemplate.setOnClickListener {
@ -1087,6 +1102,14 @@ object UiUtil {
editSelected.setOnClickListener { editSelected.setOnClickListener {
editSelectedClicked() editSelectedClicked()
} }
val shortcuts = view.findViewById<View>(R.id.shortcut)
shortcuts.isEnabled = shortcutCount > 0
shortcuts.setOnClickListener {
runBlocking {
shortcutClicked()
}
}
} }
@ -1117,6 +1140,31 @@ object UiUtil {
.build() .build()
} }
private var textHighLightSchemes = listOf(
ColorScheme(Pattern.compile("([\"'])(?:\\\\1|.)*?\\1"), Color.parseColor("#FC8500")),
ColorScheme(Pattern.compile("yt-dlp"), Color.parseColor("#00FF00")),
ColorScheme(Pattern.compile("(https?://(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?://(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"), Color.parseColor("#b5942f")),
ColorScheme(Pattern.compile("\\d+(\\.\\d)?%"), Color.parseColor("#43a564"))
)
fun View.enableTextHighlight(){
if (this is EditText || this is TextView){
//init syntax highlighter
val highlight = Highlight()
val highlightWatcher = HighlightTextWatcher()
highlight.addScheme(
*textHighLightSchemes.map { it }.toTypedArray()
)
highlightWatcher.addScheme(
*textHighLightSchemes.map { it }.toTypedArray()
)
highlight.setSpan(this as TextView)
this.addTextChangedListener(highlightWatcher)
}
}

View file

@ -31,8 +31,6 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -61,29 +59,34 @@ class DownloadWorker(
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success()
runningYTDLInstances.addAll(dao.getActiveDownloadsList().map { it.id })
val pendingIntent = NavDeepLinkBuilder(context) val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph) .setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment) .setDestination(R.id.downloadQueueMainFragment)
.createPendingIntent() .createPendingIntent()
var notification = notificationUtil.createDefaultWorkerNotification() val workNotif = notificationUtil.createDefaultWorkerNotification()
val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), notification) val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), workNotif)
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
queuedItems.collectLatest { items -> queuedItems.collectLatest { items ->
runningYTDLInstances.clear()
dao.getActiveDownloadsList().forEach {
runningYTDLInstances.add(it.id)
}
val running = ArrayList(runningYTDLInstances) val running = ArrayList(runningYTDLInstances)
if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running } val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running }
eligibleDownloads.forEach {downloadItem ->
eligibleDownloads.forEach{downloadItem ->
runningYTDLInstances.add(downloadItem.id) runningYTDLInstances.add(downloadItem.id)
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt())
notificationUtil.notify(downloadItem.id.toInt(), notification)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
withContext(Dispatchers.Main){
notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
notificationUtil.notify(downloadItem.id.toInt(), notification)
}
val request = infoUtil.buildYoutubeDLRequest(downloadItem) val request = infoUtil.buildYoutubeDLRequest(downloadItem)
downloadItem.status = DownloadRepository.Status.Active.toString() downloadItem.status = DownloadRepository.Status.Active.toString()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@ -91,7 +94,7 @@ class DownloadWorker(
updateDownloadItem(downloadItem, infoUtil, dao, resultDao) updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
} }
val cacheDir = FileUtil.getCachePath() val cacheDir = FileUtil.getCachePath(context)
val tempFileDir = File(cacheDir, downloadItem.id.toString()) val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete() tempFileDir.delete()
tempFileDir.mkdirs() tempFileDir.mkdirs()
@ -126,6 +129,7 @@ class DownloadWorker(
} }
} }
runCatching { runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id))
@ -143,6 +147,8 @@ class DownloadWorker(
} }
}.onSuccess { }.onSuccess {
runningYTDLInstances.remove(downloadItem.id) runningYTDLInstances.remove(downloadItem.id)
FileUtil.deleteConfigFiles(request)
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
runBlocking { runBlocking {
var finalPaths : List<String>? var finalPaths : List<String>?
@ -209,6 +215,8 @@ class DownloadWorker(
}.onFailure { }.onFailure {
runningYTDLInstances.remove(downloadItem.id) runningYTDLInstances.remove(downloadItem.id)
FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
} }
@ -246,6 +254,7 @@ class DownloadWorker(
} }
} }
} }
if (eligibleDownloads.isNotEmpty()){ if (eligibleDownloads.isNotEmpty()){
eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() } eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() }
dao.updateMultiple(eligibleDownloads) dao.updateMultiple(eligibleDownloads)

View file

@ -51,7 +51,7 @@ class TerminalDownloadWorker(
val intent = Intent(context, TerminalActivity::class.java) val intent = Intent(context, TerminalActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), itemId, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), itemId)
val foregroundInfo = ForegroundInfo(itemId, notification) val foregroundInfo = ForegroundInfo(itemId, notification)
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
@ -65,7 +65,7 @@ class TerminalDownloadWorker(
request.addOption( request.addOption(
"--config-locations", "--config-locations",
File(context.cacheDir.absolutePath + "/config${System.currentTimeMillis()}.txt").apply { File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply {
writeText(command) writeText(command)
}.absolutePath }.absolutePath
) )

View file

@ -13,9 +13,6 @@ class UpdateYTDLWorker(
workerParams: WorkerParameters workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val notification = NotificationUtil(context).createYTDLUpdateNotification()
val foregroundInfo = ForegroundInfo(System.currentTimeMillis().toInt(), notification)
setForeground(foregroundInfo)
UpdateUtil(context).updateYoutubeDL() UpdateUtil(context).updateYoutubeDL()
return Result.success() return Result.success()
} }

View file

@ -1,11 +1,10 @@
<LinearLayout <LinearLayout android:id="@+id/adjust"
android:id="@+id/adjust"
android:layout_width="match_parent" android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingVertical="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:orientation="vertical" android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -41,5 +40,12 @@
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.Chip
android:id="@+id/shortcut"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shortcuts"
app:chipIcon="@drawable/ic_shortcut" />
</LinearLayout> </LinearLayout>

View file

@ -179,7 +179,7 @@
style="@style/Widget.Material3.Chip.Assist.Elevated" style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:enabled="false"
android:checked="false" android:checked="false"
android:text="@string/subtitle_languages"/> android:text="@string/subtitle_languages"/>

View file

@ -1,57 +1,76 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/command_template_item_constraint" android:id="@+id/command_card"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:checkable="true"
android:orientation="horizontal" android:clickable="true"
android:background="?android:attr/selectableItemBackground" android:focusable="true"
android:padding="15dp"> android:backgroundTint="@android:color/transparent"
app:checkedIcon="@null"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp"
app:cardPreventCornerOverlap="true"
android:layout_height="wrap_content">
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title" android:layout_width="match_parent"
android:layout_width="0dp" android:layout_height="match_parent"
android:layout_height="wrap_content" android:padding="15dp">
android:layout_marginEnd="10dp"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/content" android:layout_width="0dp"
android:layout_width="0dp" android:visibility="gone"
android:layout_height="wrap_content" android:id="@+id/id"
android:layout_marginEnd="15dp" android:layout_height="0dp"
android:maxLines="2" tools:ignore="MissingConstraints" />
android:ellipsize="end"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView <TextView
android:id="@+id/useInExtraCommands" android:id="@+id/title"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" android:layout_width="0dp"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_marginEnd="10dp"
android:background="@drawable/rounded_corner" android:textSize="15sp"
android:backgroundTint="?attr/colorSecondary" android:textStyle="bold"
android:clickable="false" app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
android:ellipsize="end" app:layout_constraintTop_toTopOf="parent" />
android:text="@string/extra_command"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView
android:id="@+id/useInExtraCommands"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:visibility="gone"
android:ellipsize="end"
android:text="@string/extra_command"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -92,7 +92,7 @@
style="@style/Widget.Material3.Button.TextButton.Icon" style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:textAlignment="textStart" android:textAlignment="textStart"
android:singleLine="true" android:singleLine="true"
android:text="@string/app_name" android:text="@string/app_name"

View file

@ -5,13 +5,13 @@
<LinearLayout android:layout_width="match_parent" <LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="20dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:text="@string/format" android:text="@string/format"
android:textSize="17sp" android:textSize="18sp"
android:textStyle="bold"
android:layout_margin="20dp" android:layout_margin="20dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -23,24 +23,23 @@
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:weightSum="100" android:weightSum="100"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_weight="30"
android:maxLines="2"
android:text="@string/format_id" android:text="@string/format_id"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:layout_margin="10dp"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/format_id_value" android:id="@+id/format_id_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_weight="70"
android:textSize="18sp" android:textSize="14sp"
android:layout_marginHorizontal="10dp" android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -54,26 +53,26 @@
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:weightSum="100" android:weightSum="100"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold" android:layout_weight="30"
android:textSize="14sp" android:maxLines="2"
android:text="@string/url" android:text="@string/url"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_marginVertical="5dp" android:layout_margin="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/format_url_value" android:id="@+id/format_url_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:gravity="end" android:maxLines="3"
android:maxLines="2"
android:ellipsize="end" android:ellipsize="end"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
@ -87,23 +86,23 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold" android:layout_weight="30"
android:maxLines="2"
android:textSize="14sp" android:textSize="14sp"
android:text="@string/container" android:text="@string/container"
android:layout_marginHorizontal="10dp" android:layout_margin="10dp"
android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/container_value" android:id="@+id/container_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -117,22 +116,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold" android:layout_weight="30"
android:textSize="14sp" android:textSize="14sp"
android:text="@string/codec" android:text="@string/codec"
android:layout_marginHorizontal="10dp" android:layout_margin="10dp"
android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/codec_value" android:id="@+id/codec_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -146,22 +145,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold" android:layout_weight="30"
android:textSize="14sp"
android:text="@string/file_size" android:text="@string/file_size"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_marginVertical="5dp" android:layout_margin="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/filesize_value" android:id="@+id/filesize_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -175,22 +174,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold"
android:textSize="14sp"
android:text="@string/quality" android:text="@string/quality"
android:layout_marginHorizontal="10dp" android:layout_weight="30"
android:layout_marginVertical="5dp" android:textSize="14sp"
android:layout_margin="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/format_note_value" android:id="@+id/format_note_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -204,22 +203,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold" android:layout_weight="30"
android:textSize="14sp"
android:text="FPS" android:text="FPS"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_marginVertical="5dp" android:layout_margin="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/fps_value" android:id="@+id/fps_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -233,22 +232,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" > android:orientation="horizontal" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:textStyle="bold" android:layout_weight="30"
android:textSize="14sp"
android:text="@string/audio_samplerate" android:text="@string/audio_samplerate"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_marginVertical="5dp" android:layout_margin="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/asr_value" android:id="@+id/asr_value"
android:layout_width="match_parent" android:layout_width="0dp"
android:textSize="18sp" android:layout_weight="70"
android:layout_marginHorizontal="10dp" android:textSize="14sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>

View file

@ -7,11 +7,11 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="10dp"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/content" android:id="@+id/content"
android:layout_marginHorizontal="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="10dp" android:paddingBottom="10dp"
@ -22,32 +22,26 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:maxLines="10" android:maxLines="99999"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <TextView
android:id="@+id/template" android:layout_width="wrap_content"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="10dp" android:paddingHorizontal="10dp"
android:hint="@string/command_templates"> android:paddingTop="10dp"
android:text="@string/command" />
<AutoCompleteTextView <include layout="@layout/command_template_item" />
android:id="@+id/template_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/outputPath" android:id="@+id/outputPath"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginTop="10dp"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/save_dir" android:hint="@string/save_dir"
style="@style/Widget.Material3.TextInputLayout.FilledBox"> style="@style/Widget.Material3.TextInputLayout.FilledBox">
@ -62,7 +56,7 @@
<TextView <TextView
android:id="@+id/freespace" android:id="@+id/freespace"
android:paddingHorizontal="10dp" android:paddingHorizontal="20dp"
android:paddingVertical="5dp" android:paddingVertical="5dp"
android:textSize="13sp" android:textSize="13sp"
android:textColor="@android:color/tab_indicator_text" android:textColor="@android:color/tab_indicator_text"

View file

@ -4,11 +4,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">

View file

@ -49,7 +49,6 @@
<Preference <Preference
app:icon="@drawable/baseline_drive_file_move_24" app:icon="@drawable/baseline_drive_file_move_24"
app:key="move_cache" app:key="move_cache"
app:isPreferenceVisible="false"
android:summary="@string/move_temporary_files_summary" android:summary="@string/move_temporary_files_summary"
app:title="@string/move_temporary_files" /> app:title="@string/move_temporary_files" />

View file

@ -56,7 +56,7 @@
android:icon="@drawable/baseline_subject_24" android:icon="@drawable/baseline_subject_24"
app:key="subs_lang" app:key="subs_lang"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:defaultValue="all" app:defaultValue="en.*,.*-orig"
app:title="@string/subtitle_languages" /> app:title="@string/subtitle_languages" />