package com.deniscerri.ytdl.util import android.animation.Animator import android.animation.ObjectAnimator import android.animation.ValueAnimator 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.content.SharedPreferences import android.net.Uri import android.os.Build 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.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.Window import android.view.inputmethod.InputMethodManager import android.widget.Button import android.widget.CheckBox import android.widget.EditText import android.widget.LinearLayout import android.widget.PopupMenu import android.widget.RadioButton import android.widget.TextView import android.widget.Toast import androidx.annotation.DimenRes import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml import androidx.core.view.children import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doOnTextChanged import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.CommandTemplate import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.HistoryItem import com.deniscerri.ytdl.database.models.TemplateShortcut import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.ui.downloadcard.VideoCutListener import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.getMediaDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.extractors.PipedApiUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils 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.floatingactionbutton.FloatingActionButton import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale object UiUtil { @SuppressLint("SetTextI18n") fun populateFormatCard(context: Context, formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List? = null, showSize: Boolean = true){ 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.isBlank()) container = context.getString(R.string.defaultValue) formatCard.findViewById(R.id.container).text = container.uppercase() formatCard.findViewById(R.id.format_note).text = formatNote.uppercase() formatCard.findViewById(R.id.audio_formats).apply { if (!audioFormats.isNullOrEmpty()){ text = "id: " + audioFormats.joinToString("+") { it.format_id } visibility = View.VISIBLE }else if (chosenFormat.vcodec != "none" && chosenFormat.vcodec != "" && chosenFormat.acodec != "none" && chosenFormat.acodec != "") { text = chosenFormat.acodec visibility = View.VISIBLE }else{ visibility = View.GONE } setOnClickListener { formatCard.callOnClick() } } formatCard.findViewById(R.id.format_id).apply { text = "id: ${chosenFormat.format_id}" setOnClickListener { formatCard.callOnClick() } } val codec = if (chosenFormat.encoding != "") { chosenFormat.encoding.uppercase() }else if (chosenFormat.vcodec != "none" && chosenFormat.vcodec != ""){ chosenFormat.vcodec.uppercase() } else { chosenFormat.acodec.uppercase() } formatCard.findViewById(R.id.codec).apply { if (codec == "" || codec == "none"){ visibility = View.GONE }else{ visibility = View.VISIBLE text = codec } setOnClickListener { formatCard.callOnClick() } } var filesize = chosenFormat.filesize if (!audioFormats.isNullOrEmpty() && filesize > 10L) filesize += audioFormats.sumOf { it.filesize } formatCard.findViewById(R.id.file_size).apply { if (showSize) { text = FileUtil.convertFileSize(filesize) }else{ text = "?" } setOnClickListener { formatCard.callOnClick() } } } fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){ card.findViewById(R.id.title).text = item.title card.findViewById(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, dismissed: () -> Unit ){ val bottomSheet = BottomSheetDialog(context) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.setContentView(R.layout.create_command_template) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) 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 preferredCommandSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.preferredCommandTemplateSwitch)!! val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!! val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!! val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!! val extraCommandsDataFetchingSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandDataFetching)!! val shortcutsChipGroup : ChipGroup = bottomSheet.findViewById(R.id.shortcutsChipGroup)!! val editShortcuts : Button = bottomSheet.findViewById(R.id.edit_shortcuts)!! extraCommandsDataFetchingSwitch.isVisible = sharedPreferences.getBoolean("enable_data_fetching_extra_commands", false) if (item != null){ title.editText!!.setText(item.title) content.editText!!.setText(item.content) bottomSheet.findViewById(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.isNotEmpty() && content.editText!!.text.isNotEmpty() && if (extraCommandsSwitch.isChecked){ ((extraCommandsAudio.isChecked || extraCommandsVideo.isChecked)) }else{ true } } }) 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 (extraCommandsSwitch.isChecked){ ((extraCommandsAudio.isChecked || extraCommandsVideo.isChecked)) }else{ true } 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){ preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "") extraCommandsDataFetchingSwitch.isChecked = item.useAsExtraCommandDataFetching && extraCommandsDataFetchingSwitch.isVisible extraCommandsSwitch.isChecked = item.useAsExtraCommand if (item.useAsExtraCommand){ extraCommandsAudio.isVisible = true extraCommandsAudio.isChecked = item.useAsExtraCommandAudio extraCommandsVideo.isVisible = true extraCommandsVideo.isChecked = item.useAsExtraCommandVideo }else{ extraCommandsAudio.isVisible = false extraCommandsAudio.isChecked = false extraCommandsVideo.isVisible = false extraCommandsVideo.isChecked = false } } extraCommandsSwitch.setOnCheckedChangeListener { compoundButton, b -> extraCommandsAudio.isVisible = extraCommandsSwitch.isChecked extraCommandsAudio.isChecked = true extraCommandsVideo.isVisible = extraCommandsSwitch.isChecked extraCommandsVideo.isChecked = true } extraCommandsAudio.setOnCheckedChangeListener { compoundButton, b -> ok.isEnabled = (extraCommandsAudio.isChecked || extraCommandsVideo.isChecked) && title.editText!!.text.isNotEmpty() && content.editText!!.text.isNotEmpty() } extraCommandsVideo.setOnCheckedChangeListener { compoundButton, b -> ok.isEnabled = (extraCommandsAudio.isChecked || extraCommandsVideo.isChecked) && title.editText!!.text.isNotEmpty() && content.editText!!.text.isNotEmpty() } 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 (preferredCommandSwitch.isChecked) { sharedPreferences.edit().putString("preferred_command_template", content.editText!!.text.toString()).apply() }else if (sharedPreferences.getString("preferred_command_template", "") == item?.content){ sharedPreferences.edit().putString("preferred_command_template", "").apply() } if (item == null){ val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked, extraCommandsDataFetchingSwitch.isChecked) commandTemplateViewModel.insert(t) newTemplate(t) }else{ item.title = title.editText!!.text.toString() item.content = content.editText!!.text.toString() item.useAsExtraCommand = extraCommandsSwitch.isChecked item.useAsExtraCommandAudio = extraCommandsAudio.isChecked item.useAsExtraCommandVideo = extraCommandsVideo.isChecked item.useAsExtraCommandDataFetching = extraCommandsDataFetchingSwitch.isChecked commandTemplateViewModel.update(item) newTemplate(item) } bottomSheet.cancel() } bottomSheet.setOnDismissListener { dismissed() } 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) { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText(context.getString(R.string.url), url) clipboard.setPrimaryClip(clip) Toast.makeText(context, context.getString(R.string.link_copied_to_clipboard), Toast.LENGTH_SHORT) .show() } fun openLinkIntent(context: Context, url: String) { val i = Intent(Intent.ACTION_VIEW) i.data = Uri.parse(url) context.startActivity(i) } fun showDatePickerOnly(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){ 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 onSubmit(date) } datePicker.show(fragmentManager, "datepicker") } fun showDatePicker(fragmentManager: FragmentManager , preferences: SharedPreferences, onSubmit : (chosenDate: Calendar) -> Unit ){ val currentDate = Calendar.getInstance() currentDate.timeInMillis = (currentDate.timeInMillis - (currentDate.timeInMillis % 1800000)) + 1800000 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 showTimePicker(fragmentManager, preferences) { chosenTime -> date[Calendar.HOUR_OF_DAY] = chosenTime[Calendar.HOUR_OF_DAY] date[Calendar.MINUTE] = chosenTime[Calendar.MINUTE] onSubmit(date) } } datePicker.show(fragmentManager, "datepicker") } fun showTimePicker(fragmentManager: FragmentManager , preferences: SharedPreferences, onSubmit : (chosenTime: Calendar) -> Unit ){ val currentDate = Calendar.getInstance() currentDate.timeInMillis = (currentDate.timeInMillis - (currentDate.timeInMillis % 1800000)) + 1800000 val date = Calendar.getInstance() val lastTime = preferences.getString("latest_timepicker_date", "${currentDate.get(Calendar.HOUR_OF_DAY)}:${currentDate.get(Calendar.MINUTE)}")!! val hour = lastTime.split(":")[0].toInt() val minute = lastTime.split(":")[1].toInt() val timepicker = MaterialTimePicker.Builder() .setTimeFormat(TimeFormat.CLOCK_24H) .setHour(hour) .setMinute(minute) .build() timepicker.addOnPositiveButtonClickListener{ date[Calendar.HOUR_OF_DAY] = timepicker.hour date[Calendar.MINUTE] = timepicker.minute preferences.edit().putString("latest_timepicker_date", "${timepicker.hour}:${timepicker.minute}").apply() 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) bottomSheet.findViewById(R.id.bottom_sheet_title)?.apply { text = item.title.ifEmpty { item.url.ifEmpty { item.playlistTitle.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } } } setOnLongClickListener { showFullTextDialog(context, text.toString(), context.getString(R.string.title)) true } } bottomSheet.findViewById(R.id.bottom_sheet_author)?.apply { text = item.author setOnLongClickListener { showFullTextDialog(context, text.toString(), context.getString(R.string.author)) true } } // BUTTON ---------------------------------- val btn = bottomSheet.findViewById(R.id.download_button_type) val typeImageResource: Int = when (item.type) { DownloadViewModel.Type.audio -> { R.drawable.ic_music } DownloadViewModel.Type.video -> { R.drawable.ic_video } else -> { R.drawable.ic_terminal } } btn?.setImageResource(typeImageResource) val time = bottomSheet.findViewById(R.id.time) val thumbnail = bottomSheet.findViewById(R.id.thumbnail) val formatNote = bottomSheet.findViewById(R.id.format_note) val container = bottomSheet.findViewById(R.id.container_chip) val codec = bottomSheet.findViewById(R.id.codec) val fileSize = bottomSheet.findViewById(R.id.file_size) val command = bottomSheet.findViewById(R.id.command) when(status){ DownloadRepository.Status.Scheduled -> { 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 } } thumbnail?.isVisible = item.thumb.isNotBlank() thumbnail?.apply { isVisible = item.thumb.isNotBlank() setOnClickListener { openLinkIntent(context, item.thumb) } } if (item.type != DownloadViewModel.Type.command){ if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = View.GONE else formatNote!!.text = item.format.format_note }else{ formatNote?.isVisible = false } if (item.format.container != "") { container!!.text = item.format.container.uppercase() container.setChipIconResource(typeImageResource) }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 ytdlpUtil = YTDLPUtil(context) command?.setOnClickListener { showGeneratedCommand(context, ytdlpUtil.parseYTDLRequestString(ytdlpUtil.buildYoutubeDLRequest(item))) } val link = bottomSheet.findViewById