- 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 versionMinor = 6
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 = ""
if (versionBuild > 0){

View file

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

View file

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
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()) {
private val onItemClickListener: OnItemClickListener
@ -23,10 +24,10 @@ class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val item: ConstraintLayout
val item: MaterialCardView
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) {
val item = getItem(position)
val card = holder.cardView
card.isVisible = item != null
if (item == null) return
card.tag = item.id.toString()

View file

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
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()) {
private val onItemClickListener: OnItemClickListener
@ -24,10 +25,10 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val item: ConstraintLayout
val item: MaterialCardView
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 com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import kotlinx.coroutines.flow.Flow
@Dao
@ -23,6 +22,9 @@ interface DownloadDao {
fun getDownloadsCountByStatus(status : List<String>) : Flow<Int>
@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>
@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")
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")
fun getDownloadById(id: Long) : DownloadItem
@ -119,6 +124,12 @@ interface DownloadDao {
@Upsert
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
fun updateMultiple(items :List<DownloadItem>)

View file

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

View file

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

View file

@ -63,7 +63,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}
fun getActiveDownloads() : List<DownloadItem> {
return downloadDao.getActiveDownloadsList()
return downloadDao.getActiveAndPausedDownloadsList()
}
fun getActiveAndQueuedDownloads() : List<DownloadItem> {
@ -110,4 +110,12 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
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 videoContainer: String?
private var videoCodec: String?
private val dao: DownloadDao
enum class Type {
audio, video, command
@ -84,7 +85,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
init {
dbManager = DBManager.getInstance(application)
val dao = dbManager.downloadDao
dao = dbManager.downloadDao
repository = DownloadRepository(dao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
@ -157,13 +158,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
)
}else{
Format(
audioFormatIDPreference.first(),
audioFormatIDPreference.first().split("+").first(),
audioContainer!!,
"",
"",
"",
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 hasPreferredAudioFormats = resultItem.formats.filter { audioFormatIDPreference.contains(it.format_id) }
val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, audioFormatIDs = if (hasPreferredAudioFormats.isEmpty()) arrayListOf() else arrayListOf(hasPreferredAudioFormats.map { it.format_id }.first()))
val preferredAudioFormats = arrayListOf<String>()
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,
resultItem.url,
@ -368,10 +385,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return cloneFormat (
try {
try{
formats.filter { it.format_note.contains("audio", ignoreCase = true)}.first {
audioFormatIDPreference.contains(it.format_id) ||
it.container == audioContainer
}
formats.filter { it.format_note.contains("audio", ignoreCase = true)}.sortedBy { it.format_id }
.firstOrNull { audioFormatIDPreference.indexOfFirst { a -> a.contains(it.format_id) } >= 0 || it.container == audioContainer} ?: throw Exception()
}catch (e: Exception){
formats.last { it.format_note.contains("audio", ignoreCase = true) }
}
@ -384,14 +399,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.video -> {
return cloneFormat(
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) {
"worst" -> {
theFormats.first()
}
else /*best*/ -> {
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 -> it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) }
requirements.add { it: Format -> it.vcodec.startsWith(videoCodec!!, true) }
@ -505,6 +520,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getErroredDownloads()
}
fun getSaved() : List<DownloadItem> {
return dao.getSavedDownloadsList()
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, 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>) {

View file

@ -7,17 +7,19 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class LogViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: LogRepository
private val downloadRepository: DownloadRepository
val items: LiveData<List<LogItem>>
init {
val dao = DBManager.getInstance(application).logDao
repository = LogRepository(dao)
repository = LogRepository(DBManager.getInstance(application).logDao)
downloadRepository = DownloadRepository(DBManager.getInstance(application).downloadDao)
items = repository.items.asLiveData()
}
@ -36,10 +38,12 @@ class LogViewModel(private val application: Application) : AndroidViewModel(appl
fun delete(item: LogItem) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item)
downloadRepository.removeLogID(item.id)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
downloadRepository.removeAllLogID()
}
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.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
@ -15,10 +16,10 @@ import kotlinx.coroutines.runBlocking
class CancelDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
val message = intent.getStringExtra("cancel")
val id = intent.getIntExtra("workID", 0)
if (message != null) {
val id = intent.getIntExtra("itemID", 0)
if (id > 0) {
runCatching {
Log.e("aa", id.toString())
val notificationUtil = NotificationUtil(c)
YoutubeDL.getInstance().destroyProcessById(id.toString())
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.Context
import android.content.Intent
import android.util.Log
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.NotificationUtil
@ -13,14 +14,14 @@ import kotlinx.coroutines.launch
class PauseDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
val id = intent.getIntExtra("workID", 0)
val id = intent.getIntExtra("itemID", 0)
if (id != 0) {
runCatching {
val title = intent.getStringExtra("title")
val notificationUtil = NotificationUtil(c)
notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
notificationUtil.createResumeDownload(id, title)
val dbManager = DBManager.getInstance(c)
CoroutineScope(Dispatchers.IO).launch{
runCatching {

View file

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

View file

@ -23,6 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -427,11 +428,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
@SuppressLint("InflateParams")
private suspend fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){
searchSuggestionsLinearLayout!!.visibility = GONE
searchHistoryLinearLayout!!.visibility = GONE
searchSuggestionsLinearLayout!!.removeAllViews()
searchHistoryLinearLayout!!.removeAllViews()
searchSuggestionsLinearLayout!!.visibility = GONE
searchHistoryLinearLayout!!.visibility = GONE
linkYouCopied!!.visibility = GONE
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.util.InfoUtil
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.bottomsheet.BottomSheetBehavior
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
}
//init syntax highlighter
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.enableTextHighlight()
currentText.enableTextHighlight()
text?.setText(item?.extraCommands)
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.util.FileUtil
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.gson.Gson
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?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
@ -79,7 +82,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
}
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.editText!!.addTextChangedListener(object : TextWatcher {
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()
val commandTemplates = view.findViewById<TextInputLayout>(R.id.template)
val autoCompleteTextView =
requireView().findViewById<AutoCompleteTextView>(R.id.template_textview)
populateCommandTemplates(templates, autoCompleteTextView)
val commandTemplateCard = view.findViewById<MaterialCardView>(R.id.command_card)
populateCommandCard(commandTemplateCard, templates.first())
(commandTemplates!!.editText as AutoCompleteTextView?)!!.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long ->
chosenCommandView.editText!!.setText(templates[index].content)
downloadItem.format = Format(
templates[index].title,
"",
"",
"",
"",
0,
templates[index].content
)
commandTemplateCard.setOnClickListener {
lifecycleScope.launch {
UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel){
chosenCommandView.editText!!.setText(it.content)
populateCommandCard(commandTemplateCard, it)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
}
}
}
saveDir = view.findViewById(R.id.outputPath)
saveDir.editText!!.setText(
FileUtil.formatPath(downloadItem.downloadPath)
@ -151,9 +159,14 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace
))
val shortcutCount = withContext(Dispatchers.IO){
commandTemplateViewModel.getTotalShortcutNumber()
}
UiUtil.configureCommand(
view,
1,
shortcutCount,
newTemplateClicked = {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
@ -167,16 +180,24 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
0,
it.content
)
populateCommandTemplates(templates, autoCompleteTextView)
populateCommandCard(commandTemplateCard, it)
}
},
editSelectedClicked = {
var current = templates.find { it.title == autoCompleteTextView.text.toString() }
if (current == null) current = CommandTemplate(0, "", chosenCommandView.editText!!.text.toString(), false)
editSelectedClicked =
{
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) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
downloadItem.format = com.deniscerri.ytdlnis.database.models.Format(
it.title,
"",
"",
@ -186,29 +207,22 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
it.content
)
}
},
shortcutClicked = {
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) { sh ->
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $sh")
downloadItem.format.format_note += " $sh"
}
}
)
} catch (e: Exception) {
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(
ActivityResultContracts.StartActivityForResult()

View file

@ -75,34 +75,27 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pauseResume.setOnClickListener {
if (pauseResume.text == requireContext().getString(R.string.pause)){
lifecycleScope.launch {
workManager.cancelAllWorkByTag("download")
pauseResume.isEnabled = false
val queued = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
queued.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
runBlocking {
downloadViewModel.updateDownload(it)
}
}
queued.forEach {
workManager.cancelAllWorkByTag(it.id.toString())
}
val active = withContext(Dispatchers.IO){
// pause active
withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
active.forEach {
}.forEach {
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it)
}
// pause queued
withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it)
}
activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true
}
@ -116,17 +109,22 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
val toQueue = active.filter { it.status == DownloadRepository.Status.Paused.toString() }.toMutableList()
toQueue.forEach {
it.status = DownloadRepository.Status.Active.toString()
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
runBlocking {
downloadViewModel.queueDownloads(listOf())
}
val queuedItems = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
queuedItems.map { it.status = DownloadRepository.Status.Queued.toString() }
toQueue.addAll(queuedItems)
runBlocking {
downloadViewModel.queueDownloads(toQueue)
queuedItems.map {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
pauseResume.isEnabled = true
}
}
@ -220,20 +218,15 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
activeDownloads.notifyItemChanged(position)
val count = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.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 {
downloadViewModel.queueDownloads(listOf(item))
}
runBlocking {
downloadViewModel.queueDownloads(queue)
withContext(Dispatchers.IO){
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){
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
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.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight
import com.deniscerri.ytdlnis.work.TerminalDownloadWorker
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
@ -230,27 +231,8 @@ class TerminalActivity : BaseActivity() {
}
initMenu()
//init syntax highlighter
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(output)
highlight.setSpan(input)
input?.addTextChangedListener(highlightWatcher)
output?.addTextChangedListener(highlightWatcher)
input?.enableTextHighlight()
output?.enableTextHighlight()
}
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?.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!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
if (activeDownloadCount == 0){
File(FileUtil.getCachePath()).deleteRecursively()
File(FileUtil.getCachePath(requireContext())).deleteRecursively()
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)}"
}else{
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() )
"cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"errored" -> json.add("errored", backupErroredDownloads() )
"saved" -> json.add("saved", backupSavedDownloads() )
"cookies" -> json.add("cookies", backupCookies() )
"templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() )
@ -293,6 +294,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
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 {
runCatching {
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
if(json.has("cookies")){
val items = json.getAsJsonArray("cookies")

View file

@ -5,13 +5,9 @@ import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
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.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.getBasePath
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly
import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.DecimalFormat
@ -91,8 +85,7 @@ object FileUtil {
if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach
var destFile: DocumentFile
try {
if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)|(live_chat)".toRegex())){
it.delete()
if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(.*.part-Frag)|(.*.live_chat)".toRegex())){
return@forEach
}
@ -269,8 +262,17 @@ object FileUtil {
return listOf(context.getString(R.string.unfound_file))
}
fun getCachePath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/.CACHE"
fun getCachePath(context: Context) : String {
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{

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.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
@ -546,6 +545,7 @@ class InfoUtil(private val context: Context) {
fun getFromYTDL(query: String): ArrayList<ResultItem?> {
items = ArrayList()
val webpage_urls = mutableListOf<String>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
try {
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? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
@ -652,6 +646,12 @@ class InfoUtil(private val context: Context) {
urls = urlList.joinToString("\n")
}
val url = if (jsonObject.has("url")){
jsonObject.getString("url")
}else{
jsonObject.getString("webpage_url")
}
items.add(ResultItem(0,
url,
title,
@ -665,6 +665,10 @@ class InfoUtil(private val context: Context) {
chapters
)
)
webpage_urls.add(jsonObject.getString("webpage_url"))
}
if (items.size == 1){
items[0]?.url = webpage_urls.first()
}
} catch (e: Exception) {
Looper.prepare().run {
@ -929,7 +933,7 @@ class InfoUtil(private val context: Context) {
}
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val cacheDir = FileUtil.getCachePath()
val cacheDir = FileUtil.getCachePath(context)
val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete()
tempFileDir.mkdirs()
@ -1009,7 +1013,10 @@ class InfoUtil(private val context: Context) {
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", " ")
}
@ -1018,7 +1025,7 @@ class InfoUtil(private val context: Context) {
}
if (downloadItem.extraCommands.isNotBlank()){
val conf = File(tempFileDir.absolutePath + "/${System.currentTimeMillis()}.txt")
val conf = File(FileUtil.getCachePath(context) + "/${System.currentTimeMillis()}.txt")
conf.createNewFile()
conf.writeText(downloadItem.extraCommands)
request.addOption(
@ -1097,7 +1104,7 @@ class InfoUtil(private val context: Context) {
if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg")
if (sharedPreferences.getBoolean("crop_thumbnail", true)){
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)'\\\"\""
config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
@ -1129,39 +1136,18 @@ class InfoUtil(private val context: Context) {
}
if (downloadItem.videoPreferences.embedSubs) {
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 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"
var cont = ""
val outputContainer = downloadItem.container
if(
outputContainer.isNotEmpty() &&
outputContainer != "Default" &&
outputContainer != context.getString(R.string.defaultValue) &&
supportedContainers.contains(outputContainer) &&
!outputContainer.equals(downloadItem.format.container, true)
supportedContainers.contains(outputContainer)
){
cont = outputContainer
request.addOption("--merge-output-format", outputContainer.lowercase())
if (outputContainer != "webm") {
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){
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("--convert-subtitles", "srt")
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 -> {
request.addOption(
"--config-locations",
File(FileUtil.getCachePath() + "/config[${downloadItem.id}].txt").apply {
File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply {
writeText(downloadItem.format.format_note)
}.absolutePath
)

View file

@ -15,6 +15,7 @@ import androidx.core.content.FileProvider
import androidx.navigation.NavDeepLinkBuilder
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver
import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.ResumeActivity
import com.deniscerri.ytdlnis.receiver.ShareFileActivity
@ -81,7 +82,7 @@ class NotificationUtil(var context: Context) {
return notificationBuilder
.setContentTitle(context.getString(R.string.downloading))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
@ -98,30 +99,11 @@ class NotificationUtil(var context: Context) {
fun createDownloadServiceNotification(
pendingIntent: PendingIntent?,
title: String?,
workID: Int,
channel: String
itemID: Int,
): 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
.setContentTitle(title)
.setOngoing(true)
@ -140,13 +122,11 @@ class NotificationUtil(var context: Context) {
.setContentIntent(pendingIntent)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions()
.addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent)
.addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent)
.build()
}
fun createResumeDownload(workID: Int, title: String?, channel: String){
val notificationBuilder = getBuilder(channel)
fun createResumeDownload(itemID: Int, title: String?){
val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID)
notificationBuilder
.setContentTitle(title)
@ -162,16 +142,16 @@ class NotificationUtil(var context: Context) {
.clearActions()
val intent = Intent(context, ResumeActivity::class.java)
intent.putExtra("workID", workID)
intent.putExtra("itemID", itemID)
val resumeNotificationPendingIntent = PendingIntent.getActivity(
context,
workID,
itemID,
intent,
PendingIntent.FLAG_IMMUTABLE
)
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){
@ -315,10 +295,33 @@ class NotificationUtil(var context: Context) {
var contentText = ""
if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" + "\n"
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 {
notificationBuilder.setProgress(100, progress, false)
.setContentTitle(title)
.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())
} catch (e: Exception) {
e.printStackTrace()
@ -388,8 +391,7 @@ class NotificationUtil(var context: Context) {
fun createFormatsUpdateNotification(workID: Int): Notification {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java)
cancelIntent.putExtra("cancel", "")
val cancelIntent = Intent(context, CancelWorkReceiver::class.java)
cancelIntent.putExtra("workID", workID)
val cancelNotificationPendingIntent = PendingIntent.getBroadcast(
context,
@ -473,7 +475,7 @@ class NotificationUtil(var context: Context) {
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"
const val DOWNLOAD_FINISHED_CHANNEL_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 FORMAT_UPDATING_NOTIFICATION_ID = 6
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.DialogInterface
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.net.Uri
@ -30,7 +31,6 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
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.timepicker.MaterialTimePicker
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.runBlocking
import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.regex.Pattern
object UiUtil {
@SuppressLint("SetTextI18n")
@ -85,7 +90,7 @@ object UiUtil {
if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase()
}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_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){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -658,7 +669,7 @@ object UiUtil {
linearLayout!!.removeAllViews()
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.content).text = template.content
item.setOnClickListener {
@ -719,8 +730,12 @@ object UiUtil {
extraCommandsClicked: () -> Unit
){
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.setOnClickListener {
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
embedSubsClicked(embedSubs.isChecked)
}
@ -878,8 +893,6 @@ object UiUtil {
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)
@ -890,11 +903,11 @@ object UiUtil {
}
saveSubtitles.setOnCheckedChangeListener { _, _ ->
if (saveSubtitles.isChecked) subtitleLanguages.visibility = View.VISIBLE
else subtitleLanguages.visibility = View.GONE
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
saveSubtitlesClicked(saveSubtitles.isChecked)
}
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
subtitleLanguages.setOnClickListener {
subtitleLanguagesClicked()
}
@ -1074,8 +1087,10 @@ object UiUtil {
fun configureCommand(
view: View,
size: Int,
shortcutCount: Int,
newTemplateClicked: () -> Unit,
editSelectedClicked: () -> Unit,
shortcutClicked: suspend () -> Unit,
){
val newTemplate : Chip = view.findViewById(R.id.newTemplate)
newTemplate.setOnClickListener {
@ -1087,6 +1102,14 @@ object UiUtil {
editSelected.setOnClickListener {
editSelectedClicked()
}
val shortcuts = view.findViewById<View>(R.id.shortcut)
shortcuts.isEnabled = shortcutCount > 0
shortcuts.setOnClickListener {
runBlocking {
shortcutClicked()
}
}
}
@ -1117,6 +1140,31 @@ object UiUtil {
.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.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -61,29 +59,34 @@ class DownloadWorker(
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success()
runningYTDLInstances.addAll(dao.getActiveDownloadsList().map { it.id })
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.createPendingIntent()
var notification = notificationUtil.createDefaultWorkerNotification()
val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), notification)
val workNotif = notificationUtil.createDefaultWorkerNotification()
val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), workNotif)
setForegroundAsync(foregroundInfo)
queuedItems.collectLatest { items ->
runningYTDLInstances.clear()
dao.getActiveDownloadsList().forEach {
runningYTDLInstances.add(it.id)
}
val running = ArrayList(runningYTDLInstances)
if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
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)
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt())
notificationUtil.notify(downloadItem.id.toInt(), notification)
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)
downloadItem.status = DownloadRepository.Status.Active.toString()
CoroutineScope(Dispatchers.IO).launch {
@ -91,7 +94,7 @@ class DownloadWorker(
updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
}
val cacheDir = FileUtil.getCachePath()
val cacheDir = FileUtil.getCachePath(context)
val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete()
tempFileDir.mkdirs()
@ -126,6 +129,7 @@ class DownloadWorker(
}
}
runCatching {
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))
@ -143,6 +147,8 @@ class DownloadWorker(
}
}.onSuccess {
runningYTDLInstances.remove(downloadItem.id)
FileUtil.deleteConfigFiles(request)
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
runBlocking {
var finalPaths : List<String>?
@ -209,6 +215,8 @@ class DownloadWorker(
}.onFailure {
runningYTDLInstances.remove(downloadItem.id)
FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
}
@ -246,6 +254,7 @@ class DownloadWorker(
}
}
}
if (eligibleDownloads.isNotEmpty()){
eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() }
dao.updateMultiple(eligibleDownloads)

View file

@ -51,7 +51,7 @@ class TerminalDownloadWorker(
val intent = Intent(context, TerminalActivity::class.java)
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)
setForegroundAsync(foregroundInfo)
@ -65,7 +65,7 @@ class TerminalDownloadWorker(
request.addOption(
"--config-locations",
File(context.cacheDir.absolutePath + "/config${System.currentTimeMillis()}.txt").apply {
File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply {
writeText(command)
}.absolutePath
)

View file

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

View file

@ -1,11 +1,10 @@
<LinearLayout
android:id="@+id/adjust"
<LinearLayout android:id="@+id/adjust"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingVertical="10dp"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
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
android:layout_width="wrap_content"
@ -41,5 +40,12 @@
</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"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:enabled="false"
android:checked="false"
android:text="@string/subtitle_languages"/>

View file

@ -1,57 +1,76 @@
<?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: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_height="wrap_content"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:padding="15dp">
android:checkable="true"
android:clickable="true"
android:focusable="true"
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
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
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" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp">
<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:layout_width="0dp"
android:visibility="gone"
android:id="@+id/id"
android:layout_height="0dp"
tools:ignore="MissingConstraints" />
<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" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
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" />
</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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_marginHorizontal="10dp"
android:textAlignment="textStart"
android:singleLine="true"
android:text="@string/app_name"

View file

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

View file

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

View file

@ -4,11 +4,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
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:android="http://schemas.android.com/apk/res/android">

View file

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

View file

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