You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ytdl-android/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt

1218 lines
51 KiB
Kotlin

package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.net.Uri
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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")
fun populateFormatCard(context: Context, formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>?){
var formatNote = chosenFormat.format_note
if (formatNote.isEmpty()) formatNote = context.getString(R.string.defaultValue)
else if (formatNote == "best") formatNote = context.getString(R.string.best_quality)
else if (formatNote == "worst") formatNote = context.getString(R.string.worst_quality)
var container = chosenFormat.container
if (container == "Default") container = context.getString(R.string.defaultValue)
formatCard.findViewById<TextView>(R.id.container).text = container.uppercase()
if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase()
}else{
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}"
val codec =
if (chosenFormat.encoding != "") {
chosenFormat.encoding.uppercase()
}else if (chosenFormat.vcodec != "none" && chosenFormat.vcodec != ""){
chosenFormat.vcodec.uppercase()
} else {
chosenFormat.acodec.uppercase()
}
if (codec == "" || codec == "none"){
formatCard.findViewById<TextView>(R.id.codec).visibility = View.GONE
}else{
formatCard.findViewById<TextView>(R.id.codec).visibility = View.VISIBLE
formatCard.findViewById<TextView>(R.id.codec).text = codec
}
var filesize = chosenFormat.filesize
if (!audioFormats.isNullOrEmpty()) filesize += audioFormats.sumOf { it.filesize }
formatCard.findViewById<TextView>(R.id.file_size).text = FileUtil.convertFileSize(filesize)
}
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.alpha = 1f
card.tag = item.id
}
fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.create_command_template)
val ok : Button = bottomSheet.findViewById(R.id.template_create)!!
val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!!
val content : TextInputLayout = bottomSheet.findViewById(R.id.content)!!
val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!!
val shortcutsChipGroup : ChipGroup = bottomSheet.findViewById(R.id.shortcutsChipGroup)!!
val editShortcuts : Button = bottomSheet.findViewById(R.id.edit_shortcuts)!!
if (item != null){
title.editText!!.setText(item.title)
content.editText!!.setText(item.content)
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_subtitle)!!.text = content.resources.getString(R.string.update_template)
ok.text = content.resources.getString(R.string.update)
ok.isEnabled = true
content.endIconDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_delete_all)
}else{
ok.isEnabled = false
content.endIconDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_clipboard)
}
title.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(p0: Editable?) {
ok.isEnabled =
!(title.editText!!.text.isEmpty() || content.editText!!.text.isEmpty())
}
})
content.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(p0: Editable?) {
ok.isEnabled =
(title.editText!!.text.isNotEmpty() && content.editText!!.text.isNotEmpty())
if (content.editText!!.text.isNotEmpty()){
content.endIconDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_delete_all)
}else{
content.endIconDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_clipboard)
}
}
})
content.setEndIconOnClickListener {
if(content.editText!!.text.isEmpty()){
val clipboard: ClipboardManager =
context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
content.editText!!.setText(clipboard.primaryClip?.getItemAt(0)?.text)
}else{
content.editText!!.setText("")
}
}
if (item != null){
extraCommandsSwitch.isChecked = item.useAsExtraCommand
}
commandTemplateViewModel.shortcuts.observe(lifeCycle){
shortcutsChipGroup.removeAllViews()
it.forEach {shortcut ->
val chip = context.layoutInflater.inflate(R.layout.suggestion_chip, shortcutsChipGroup, false) as Chip
chip.text = shortcut.content
chip.setOnClickListener {
content.editText!!.text.insert(content.editText!!.selectionStart, shortcut.content + " ")
}
shortcutsChipGroup.addView(chip)
}
}
editShortcuts.setOnClickListener {
showShortcutsSheet(context, lifeCycle, commandTemplateViewModel)
}
ok.setOnClickListener {
if (item == null){
val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked)
commandTemplateViewModel.insert(t)
newTemplate(t)
}else{
item.title = title.editText!!.text.toString()
item.content = content.editText!!.text.toString()
item.useAsExtraCommand = extraCommandsSwitch.isChecked
Log.e("aa", item.toString())
commandTemplateViewModel.update(item)
newTemplate(item)
}
bottomSheet.cancel()
}
bottomSheet.show()
bottomSheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun showShortcutsSheet(context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.template_shortcuts)
val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!!
val shortcutsChipGroup : ChipGroup = bottomSheet.findViewById(R.id.shortcutsChipGroup)!!
title.isEndIconVisible = false
title.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(p0: Editable?) {
title.isEndIconVisible = p0!!.isNotEmpty()
}
})
title.setEndIconOnClickListener {
commandTemplateViewModel.insertShortcut(TemplateShortcut(0, title.editText!!.text.toString()))
title.editText!!.setText("")
title.isEndIconVisible = false
}
commandTemplateViewModel.shortcuts.observe(lifeCycle){
shortcutsChipGroup.removeAllViews()
it.forEach {shortcut ->
val chip = context.layoutInflater.inflate(R.layout.input_chip, shortcutsChipGroup, false) as Chip
chip.text = shortcut.content
chip.setOnClickListener{
commandTemplateViewModel.deleteShortcut(shortcut)
}
shortcutsChipGroup.addView(chip)
}
}
bottomSheet.show()
bottomSheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun copyLinkToClipBoard(context: Context, url: String, bottomSheet: BottomSheetDialog?) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(context.getString(R.string.url), url)
clipboard.setPrimaryClip(clip)
bottomSheet?.hide()
Toast.makeText(context, context.getString(R.string.link_copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
fun openLinkIntent(context: Context, url: String, bottomSheet: BottomSheetDialog?) {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(url)
bottomSheet?.hide()
context.startActivity(i)
}
fun openFileIntent(context: Context, downloadPath: String) {
val uri = downloadPath.runCatching {
DocumentFile.fromSingleUri(context, Uri.parse(downloadPath)).run{
if (this?.exists() == true){
this.uri
}else if (File(this@runCatching).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(this@runCatching))
}else null
}
}.getOrNull()
if (uri == null){
Toast.makeText(context, "Error opening file!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_VIEW
data = uri
}.run{
context.startActivity(this)
}
}
}
fun shareFileIntent(context: Context, paths: List<String>){
val uris : ArrayList<Uri> = arrayListOf()
paths.runCatching {
this.forEach {path ->
val uri = DocumentFile.fromSingleUri(context, Uri.parse(path)).run{
if (this?.exists() == true){
this.uri
}else if (File(path).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(path))
}else null
}
if (uri != null) uris.add(uri)
}
}
if (uris.isEmpty()){
Toast.makeText(context, "Error sharing files!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
type = if (uris.size == 1) uris[0].let { context.contentResolver.getType(it) } ?: "media/*" else "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}.run{
context.startActivity(Intent.createChooser(this, context.getString(R.string.share)))
}
}
}
fun showDatePicker(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){
val currentDate = Calendar.getInstance()
val date = Calendar.getInstance()
val datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(
CalendarConstraints.Builder()
.setValidator(DateValidatorPointForward.now())
.build()
)
.setSelection(MaterialDatePicker.todayInUtcMilliseconds())
.build()
datePicker.addOnPositiveButtonClickListener{
date.timeInMillis = it
val timepicker = MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_24H)
.setHour(currentDate.get(Calendar.HOUR_OF_DAY))
.setMinute(currentDate.get(Calendar.MINUTE))
.build()
timepicker.addOnPositiveButtonClickListener{
date[Calendar.HOUR_OF_DAY] = timepicker.hour
date[Calendar.MINUTE] = timepicker.minute
onSubmit(date)
}
timepicker.show(fragmentManager, "timepicker")
}
datePicker.show(fragmentManager, "datepicker")
}
fun showTimePicker(fragmentManager: FragmentManager , onSubmit : (chosenTime: Calendar) -> Unit ){
val currentDate = Calendar.getInstance()
val date = Calendar.getInstance()
val timepicker = MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_24H)
.setHour(currentDate.get(Calendar.HOUR_OF_DAY))
.setMinute(currentDate.get(Calendar.MINUTE))
.build()
timepicker.addOnPositiveButtonClickListener{
date[Calendar.HOUR_OF_DAY] = timepicker.hour
date[Calendar.MINUTE] = timepicker.minute
onSubmit(date)
}
timepicker.show(fragmentManager, "timepicker")
}
fun showDownloadItemDetailsCard(
item: DownloadItem,
context: Activity,
status: DownloadRepository.Status,
removeItem : (DownloadItem, BottomSheetDialog) -> Unit,
downloadItem: (DownloadItem) -> Unit,
longClickDownloadButton: (DownloadItem) -> Unit,
scheduleButtonClick: (DownloadItem) -> Unit?
){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
title!!.text = item.title.ifEmpty { "`${context.getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" }
// BUTTON ----------------------------------
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
when (item.type) {
DownloadViewModel.Type.audio -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_terminal)
}
}
val time = bottomSheet.findViewById<Chip>(R.id.time)
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
when(status){
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> {
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
else {
val calendar = Calendar.getInstance()
calendar.timeInMillis = item.downloadStartTime
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
time.setOnClickListener {
scheduleButtonClick(item)
bottomSheet.dismiss()
}
}
}
else -> {
time!!.visibility = View.GONE
}
}
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
else formatNote!!.text = item.format.format_note
if (item.format.container != "") container!!.text = item.format.container.uppercase()
else container!!.visibility = View.GONE
val codecText =
if (item.format.encoding != "") {
item.format.encoding.uppercase()
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
item.format.vcodec.uppercase()
} else {
item.format.acodec.uppercase()
}
if (codecText == "" || codecText == "none"){
codec!!.visibility = View.GONE
}else{
codec!!.visibility = View.VISIBLE
codec.text = codecText
}
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
else fileSize!!.text = fileSizeReadable
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url
link!!.text = url
link.tag = item.id
link.setOnClickListener{
openLinkIntent(context, item.url, bottomSheet)
}
link.setOnLongClickListener{
copyLinkToClipBoard(context, item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = item.id
remove.setOnClickListener{
removeItem(item, bottomSheet)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val download = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
download?.tag = item.id
when(status){
DownloadRepository.Status.Cancelled, DownloadRepository.Status.Saved -> {
download!!.text = context.getString(R.string.download)
download.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
download.setOnLongClickListener {
longClickDownloadButton(item)
bottomSheet.cancel()
true
}
}
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> {
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) download!!.visibility = View.GONE
else{
download!!.text = context.getString(R.string.download_now)
}
}
else -> {
download?.setOnLongClickListener {
longClickDownloadButton(item)
bottomSheet.cancel()
true
}
}
}
download?.setOnClickListener {
bottomSheet.dismiss()
downloadItem(item)
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
context.windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun showFormatDetails(format: Format, activity: Activity){
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.format_details_sheet)
val formatIdParent = bottomSheet.findViewById<LinearLayout>(R.id.format_id_parent)
val formatURLParent = bottomSheet.findViewById<LinearLayout>(R.id.format_url_parent)
val containerParent = bottomSheet.findViewById<LinearLayout>(R.id.container_parent)
val codecParent = bottomSheet.findViewById<LinearLayout>(R.id.codec_parent)
val filesizeParent = bottomSheet.findViewById<LinearLayout>(R.id.filesize_parent)
val formatnoteParent = bottomSheet.findViewById<LinearLayout>(R.id.format_note_parent)
val fpsParent = bottomSheet.findViewById<LinearLayout>(R.id.fps_parent)
val asrParent = bottomSheet.findViewById<LinearLayout>(R.id.asr_parent)
if (format.format_id.isBlank()) formatIdParent?.visibility = View.GONE
else {
formatIdParent?.findViewById<TextView>(R.id.format_id_value)?.text = format.format_id
formatIdParent?.setOnClickListener {
copyToClipboard(format.format_id, activity)
}
}
if (format.url.isNullOrBlank()) formatURLParent?.visibility = View.GONE
else {
formatURLParent?.findViewById<TextView>(R.id.format_url_value)?.text = format.url
formatURLParent?.setOnClickListener {
copyToClipboard(format.url!!, activity)
}
}
if (format.container.isBlank()) containerParent?.visibility = View.GONE
else {
containerParent?.findViewById<TextView>(R.id.container_value)?.text = format.container
containerParent?.setOnClickListener {
copyToClipboard(format.container, activity)
}
}
val codecField =
if (format.encoding != "") {
format.encoding.uppercase()
}else if (format.vcodec != "none" && format.vcodec != ""){
format.vcodec.uppercase()
} else {
format.acodec.uppercase()
}
if (codecField.isBlank()) codecParent?.visibility = View.GONE
else {
codecParent?.findViewById<TextView>(R.id.codec_value)?.text = codecField
codecParent?.setOnClickListener {
copyToClipboard(codecField, activity)
}
}
if (format.filesize != 0L) filesizeParent?.visibility = View.GONE
else {
filesizeParent?.findViewById<TextView>(R.id.filesize_value)?.text = FileUtil.convertFileSize(format.filesize)
filesizeParent?.setOnClickListener {
copyToClipboard(FileUtil.convertFileSize(format.filesize), activity)
}
}
if (format.format_note.isBlank()) formatnoteParent?.visibility = View.GONE
else {
formatnoteParent?.findViewById<TextView>(R.id.format_note_value)?.text = format.format_note
formatnoteParent?.setOnClickListener {
copyToClipboard(format.format_note, activity)
}
}
if (format.fps.isNullOrBlank() || format.fps == "0") fpsParent?.visibility = View.GONE
else {
fpsParent?.findViewById<TextView>(R.id.fps_value)?.text = format.fps
fpsParent?.setOnClickListener {
copyToClipboard(format.fps!!, activity)
}
}
if (format.asr.isNullOrBlank()) asrParent?.visibility = View.GONE
else {
asrParent?.findViewById<TextView>(R.id.asr_value)?.text = format.asr
asrParent?.setOnClickListener {
copyToClipboard(format.asr!!, activity)
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun showSubtitleLanguagesDialog(context: Activity, currentValue: String, ok: (newValue: String) -> Unit){
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.subtitle_languages))
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.subtitle_languages)
editText.setText(currentValue)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
context.getString(R.string.ok)
) { dialog: DialogInterface?, which: Int ->
ok(editText.text.toString())
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> }
builder.setNeutralButton("?") { dialog: DialogInterface?, which: Int ->
val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/yt-dlp/yt-dlp#subtitle-options"))
context.startActivity(browserIntent)
}
val dialog = builder.create()
editText.doOnTextChanged { text, start, before, count ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText.setSelection(editText.text!!.length)
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
private fun copyToClipboard(text: String, activity: Activity){
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(text, text)
clipboard.setPrimaryClip(clip)
Toast.makeText(activity, activity.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: CommandTemplate) -> Unit) {
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.command_template_list)
val linearLayout = bottomSheet.findViewById<LinearLayout>(R.id.command_list_linear_layout)
val list = withContext(Dispatchers.IO){
commandTemplateViewModel.getAll()
}
linearLayout!!.removeAllViews()
list.forEach {template ->
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 {
itemSelected(template)
bottomSheet.cancel()
}
linearLayout.addView(item)
}
bottomSheet.show()
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
suspend fun showShortcuts(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: String) -> Unit){
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.template_shortcuts_list)
val chipGroup = bottomSheet.findViewById<ChipGroup>(R.id.shortcutsChipGroup)
val shortcutList = withContext(Dispatchers.IO){
commandTemplateViewModel.getAllShortcuts()
}
chipGroup!!.removeAllViews()
shortcutList.forEach {shortcut ->
val chip = activity.layoutInflater.inflate(R.layout.suggestion_chip, chipGroup, false) as Chip
chip.text = shortcut.content
chip.setOnClickListener {
itemSelected(shortcut.content)
}
chipGroup.addView(chip)
}
bottomSheet.show()
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun configureVideo(
view: View,
context: Activity,
items: List<DownloadItem>,
embedSubsClicked : (Boolean) -> Unit,
addChaptersClicked: (Boolean) -> Unit,
splitByChaptersClicked: (Boolean) -> Unit,
saveThumbnailClicked: (Boolean) -> Unit,
sponsorBlockItemsSet: (values: Array<String>, checkedItems: List<Boolean>) -> Unit,
cutClicked: (VideoCutListener) -> Unit,
filenameTemplateSet: (String) -> Unit,
saveSubtitlesClicked: (Boolean) -> Unit,
subtitleLanguagesClicked: () -> Unit,
removeAudioClicked: (Boolean) -> Unit,
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)
}
val addChapters = view.findViewById<Chip>(R.id.add_chapters)
addChapters!!.isChecked = items.all { it.videoPreferences.addChapters }
addChapters.setOnClickListener{
addChaptersClicked(addChapters.isChecked)
}
val splitByChapters = view.findViewById<Chip>(R.id.split_by_chapters)
if(items.size == 1 && items[0].downloadSections.isNotBlank()){
splitByChapters.isEnabled = false
splitByChapters.isChecked = false
}else{
splitByChapters!!.isChecked = items.all { it.videoPreferences.splitByChapters }
}
if (splitByChapters.isChecked){
items.forEach { it.videoPreferences.addChapters = false }
addChapters.isChecked = false
}
splitByChapters.setOnClickListener {
if (splitByChapters.isChecked){
addChapters.isEnabled = false
addChapters.isChecked = false
addChaptersClicked(false)
}else{
addChapters.isEnabled = true
}
splitByChaptersClicked(splitByChapters.isChecked)
}
val saveThumbnail = view.findViewById<Chip>(R.id.save_thumbnail)
saveThumbnail!!.isChecked = items.all { it.SaveThumb }
saveThumbnail.setOnClickListener {
saveThumbnailClicked(saveThumbnail.isChecked)
}
val sponsorBlock = view.findViewById<Chip>(R.id.sponsorblock_filters)
sponsorBlock!!.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.select_sponsorblock_filtering))
val values = context.resources.getStringArray(R.array.sponsorblock_settings_values)
val entries = context.resources.getStringArray(R.array.sponsorblock_settings_entries)
val checkedItems : ArrayList<Boolean> = arrayListOf()
values.forEach {
if (items.all{ i -> i.videoPreferences.sponsorBlockFilters.contains(it)}) {
checkedItems.add(true)
}else{
checkedItems.add(false)
}
}
builder.setMultiChoiceItems(
entries,
checkedItems.toBooleanArray()
) { _, which, isChecked ->
checkedItems[which] = isChecked
}
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
sponsorBlockItemsSet(values, checkedItems)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
val cut = view.findViewById<Chip>(R.id.cut)
if (items.size > 1) cut.isVisible = false
else{
if(items[0].duration.isNotEmpty()){
val downloadItem = items[0]
cut.isEnabled = true
if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections
val cutVideoListener = object : VideoCutListener {
override fun onChangeCut(list: List<String>) {
if (list.isEmpty()){
downloadItem.downloadSections = ""
cut.text = context.getString(R.string.cut)
splitByChapters.isEnabled = true
splitByChapters.isChecked = downloadItem.videoPreferences.splitByChapters
if (splitByChapters.isChecked){
addChapters.isEnabled = false
addChapters.isChecked = false
}else{
addChapters.isEnabled = true
}
}else{
var value = ""
list.forEach {
value += "$it;"
}
downloadItem.downloadSections = value
cut.text = value.dropLast(1)
splitByChapters.isEnabled = false
splitByChapters.isChecked = false
addChapters.isEnabled = true
}
}
}
cut.setOnClickListener {
cutClicked(cutVideoListener)
}
}else{
cut.isEnabled = false
}
}
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.file_name_template))
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
if (items.size == 1){
editText.setText(items[0].customFileNameTemplate)
}
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameTemplateSet(editText.text.toString())
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
editText.doOnTextChanged { _, _, _, _ ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
items.forEach { it.videoPreferences.subsLanguages = sharedPreferences.getString("subs_lang", "en.*,.*-orig")!! }
if (items.all { it.videoPreferences.writeSubs}) {
saveSubtitles.isChecked = true
subtitleLanguages.visibility = View.VISIBLE
}
saveSubtitles.setOnCheckedChangeListener { _, _ ->
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
saveSubtitlesClicked(saveSubtitles.isChecked)
}
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
subtitleLanguages.setOnClickListener {
subtitleLanguagesClicked()
}
val removeAudio = view.findViewById<Chip>(R.id.remove_audio)
removeAudio.isChecked = items.all { it.videoPreferences.removeAudio }
removeAudio.setOnCheckedChangeListener { _, _ ->
removeAudioClicked(removeAudio.isChecked)
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands.visibility = View.VISIBLE
extraCommands.setOnClickListener {
extraCommandsClicked()
}
}else{
extraCommands.visibility = View.GONE
}
}
fun configureAudio(
view: View,
context: Activity,
items: List<DownloadItem>,
embedThumbClicked: (Boolean) -> Unit,
splitByChaptersClicked: (Boolean) -> Unit,
filenameTemplateSet: (String) -> Unit,
sponsorBlockItemsSet: (Array<String>, List<Boolean>) -> Unit,
cutClicked: (VideoCutListener) -> Unit,
extraCommandsClicked: () -> Unit,
){
val embedThumb = view.findViewById<Chip>(R.id.embed_thumb)
embedThumb!!.isChecked = items.all { it.audioPreferences.embedThumb }
embedThumb.setOnClickListener {
embedThumbClicked(embedThumb.isChecked)
}
val splitByChapters = view.findViewById<Chip>(R.id.split_by_chapters)
if (items.size == 1 && items[0].downloadSections.isNotBlank()){
splitByChapters.isEnabled = false
splitByChapters.isChecked = false
}else{
splitByChapters!!.isChecked = items.all { it.audioPreferences.splitByChapters }
}
splitByChapters.setOnClickListener {
splitByChaptersClicked(splitByChapters.isChecked)
}
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.file_name_template))
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
if (items.size == 1){
editText.setText(items[0].customFileNameTemplate)
}
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameTemplateSet(editText.text.toString())
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
editText.doOnTextChanged { _, _, _, _ ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
val sponsorBlock = view.findViewById<Chip>(R.id.sponsorblock_filters)
sponsorBlock!!.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.select_sponsorblock_filtering))
val values = context.resources.getStringArray(R.array.sponsorblock_settings_values)
val entries = context.resources.getStringArray(R.array.sponsorblock_settings_entries)
val checkedItems : ArrayList<Boolean> = arrayListOf()
values.forEach {
if (items.all{ i -> i.audioPreferences.sponsorBlockFilters.contains(it)}) {
checkedItems.add(true)
}else{
checkedItems.add(false)
}
}
builder.setMultiChoiceItems(
entries,
checkedItems.toBooleanArray()
) { _, which, isChecked ->
checkedItems[which] = isChecked
}
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
sponsorBlockItemsSet(values, checkedItems)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
val cut = view.findViewById<Chip>(R.id.cut)
if (items.size > 1) cut.isVisible = false
else{
val downloadItem = items[0]
if (downloadItem.duration.isNotEmpty()){
cut.isEnabled = true
if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections
val cutVideoListener = object : VideoCutListener {
override fun onChangeCut(list: List<String>) {
if (list.isEmpty()){
downloadItem.downloadSections = ""
cut.text = context.getString(R.string.cut)
splitByChapters.isEnabled = true
splitByChapters.isChecked = downloadItem.audioPreferences.splitByChapters
}else{
var value = ""
list.forEach {
value += "$it;"
}
downloadItem.downloadSections = value
cut.text = value.dropLast(1)
splitByChapters.isEnabled = false
splitByChapters.isChecked = false
}
}
}
cut.setOnClickListener {
cutClicked(cutVideoListener)
}
}else{
cut.isEnabled = false
}
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands.visibility = View.VISIBLE
extraCommands.setOnClickListener {
extraCommandsClicked()
}
}else{
extraCommands.visibility = View.GONE
}
}
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 {
newTemplateClicked()
}
val editSelected : Chip = view.findViewById(R.id.editSelected)
editSelected.isEnabled = size == 1
editSelected.setOnClickListener {
editSelectedClicked()
}
val shortcuts = view.findViewById<View>(R.id.shortcut)
shortcuts.isEnabled = shortcutCount > 0
shortcuts.setOnClickListener {
runBlocking {
shortcutClicked()
}
}
}
@SuppressLint("ClickableViewAccessibility")
fun RecyclerView.forceFastScrollMode()
{
overScrollMode = View.OVER_SCROLL_ALWAYS
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
isVerticalScrollBarEnabled = true
setOnTouchListener { view, event ->
if (event.x >= this.width - 30) {
view.parent.requestDisallowInterceptTouchEvent(true)
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_UP -> view.parent.requestDisallowInterceptTouchEvent(false)
}
}
false
}
}
fun RecyclerView.enableFastScroll(){
val drawable = ShapeDrawable(OvalShape())
drawable.paint.color = context.getColor(android.R.color.transparent)
FastScrollerBuilder(this)
.setTrackDrawable(drawable)
.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)
}
}
fun EditText.setTextAndRecalculateWidth(t : String){
val scale = context.resources.displayMetrics.density
this.setText(t)
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
this.measure(widthMeasureSpec, heightMeasureSpec)
val requiredWidth: Int = this.measuredWidth
if (t.length < 5){
this.layoutParams.width = (70 * scale + 0.5F).toInt()
}else{
this.layoutParams.width = requiredWidth
}
this.requestLayout()
}
}