diff --git a/app/build.gradle b/app/build.gradle index e7a9b29c..9f690dd4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -139,7 +139,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.2" + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.4" // implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" // implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" // implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer" @@ -150,16 +150,16 @@ dependencies { implementation "io.github.junkfood02.youtubedl-android:aria2c:0.17.2" implementation "androidx.appcompat:appcompat:$appCompatVer" - implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "androidx.constraintlayout:constraintlayout:2.2.0" implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation "androidx.navigation:navigation-fragment-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer" - implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.test.ext:junit-ktx:1.2.1' - implementation 'androidx.compose.ui:ui-android:1.7.4' + implementation 'androidx.compose.ui:ui-android:1.7.6' implementation 'androidx.preference:preference-ktx:1.2.1' testImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer" @@ -184,7 +184,7 @@ dependencies { implementation 'androidx.paging:paging-runtime-ktx:3.3.2' implementation "androidx.room:room-paging:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7' implementation "androidx.compose.runtime:runtime:$composeVer" androidTestImplementation 'com.google.truth:truth:1.1.5' @@ -196,7 +196,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7" implementation "com.neoutils.highlight:highlight-view:2.2.0" // For media playback using ExoPlayer @@ -212,8 +212,8 @@ dependencies { implementation "com.anggrayudi:storage:1.5.5" implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'com.google.accompanist:accompanist-webview:0.30.1' - implementation 'androidx.compose.material3:material3-android:1.3.0' + implementation 'androidx.compose.material3:material3-android:1.3.1' implementation "io.noties.markwon:core:4.6.2" implementation("org.greenrobot:eventbus:3.3.1") - implementation("com.github.teamnewpipe:newpipeextractor:0.24.2") + implementation("com.github.teamnewpipe:newpipeextractor:0.24.4") } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/YoutubePlayerClientItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/YoutubePlayerClientItem.kt new file mode 100644 index 00000000..d9cd3061 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/YoutubePlayerClientItem.kt @@ -0,0 +1,12 @@ +package com.deniscerri.ytdl.database.models + +data class YoutubePlayerClientItem( + var playerClient: String, + var poTokens: MutableList, + var enabled: Boolean = true +) + +data class YoutubePoTokenItem( + var context: String, + var token: String +) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt new file mode 100644 index 00000000..a1a78d7f --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt @@ -0,0 +1,121 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.app.Activity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.CookieItem +import com.deniscerri.ytdl.database.models.Format +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.databinding.FormatItemBinding +import com.deniscerri.ytdl.ui.adapter.HistoryPaginatedAdapter.ViewHolder +import com.deniscerri.ytdl.util.Extensions.popup +import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.card.MaterialCardView + +class FormatAdapter(onItemClickListener: OnItemClickListener, activity: Activity, private val downloadType: DownloadViewModel.Type) : ListAdapter(AsyncDifferConfig.Builder( + DIFF_CALLBACK +).build()) { + private val onItemClickListener: OnItemClickListener + private val activity: Activity + private var selectedVideoFormat: Format? + private val selectedAudioFormats: MutableList + private val usingGrid: Boolean + + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + this.selectedVideoFormat = null + this.usingGrid = false + this.selectedAudioFormats = mutableListOf() + } + + class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { + val item: MaterialCardView + + init { + item = itemView.findViewById(R.id.format_card_constraintLayout) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return if (usingGrid){ + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.format_item_grid, parent, false) + + ViewHolder( + cardView, + onItemClickListener + ) + }else{ + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.format_item, parent, false) + + ViewHolder( + cardView, + onItemClickListener + ) + } + } + + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) ?: return + val card = holder.item + card.popup() + UiUtil.populateFormatCard(activity, card, item) + card.setOnClickListener { + when(downloadType) { + DownloadViewModel.Type.audio -> { + onItemClickListener.onItemClick(item) + } + DownloadViewModel.Type.video -> { + if(item.isAudio()) { + if (card.isChecked) { + selectedAudioFormats.remove(item) + }else { + selectedAudioFormats.add(item) + } + }else { + if (card.isChecked) { + onItemClickListener.onItemClick(item) + }else { + selectedVideoFormat = item + card.isChecked = true + } + } + } + else -> {} + } + } + + + } + + private fun Format.isAudio() : Boolean { + return this.vcodec.isNotBlank() && this.vcodec != "none" + } + + interface OnItemClickListener { + fun onItemClick(item: Format) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Format, newItem: Format): Boolean { + return oldItem.format_id == newItem.format_id + } + + override fun areContentsTheSame(oldItem: Format, newItem: Format): Boolean { + return oldItem.format_id == newItem.format_id + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/YoutubePlayerClientAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/YoutubePlayerClientAdapter.kt new file mode 100644 index 00000000..9da61c46 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/YoutubePlayerClientAdapter.kt @@ -0,0 +1,126 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.SharedPreferences +import android.content.res.ColorStateList +import android.graphics.Color +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.CommandTemplate +import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem +import com.deniscerri.ytdl.util.Extensions.popup +import com.google.android.material.card.MaterialCardView +import com.google.android.material.materialswitch.MaterialSwitch + +class YoutubePlayerClientAdapter(onItemClickListener: OnItemClickListener, activity: Activity, private var itemTouchHelper: ItemTouchHelper) : ListAdapter(AsyncDifferConfig.Builder( + DIFF_CALLBACK +).build()) { + private val onItemClickListener: OnItemClickListener + private val activity: Activity + var showDragHandle: Boolean + private val sharedPreferences: SharedPreferences + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + this.showDragHandle = false + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) + } + + class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { + val item: MaterialCardView + + init { + item = itemView.findViewById(R.id.card) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.player_client_item, parent, false) + return ViewHolder(cardView, onItemClickListener) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position)!! + holder.itemView.tag = item.playerClient + val card = holder.item + val title = card.findViewById(R.id.title) + title.text = item.playerClient + + val content = card.findViewById(R.id.content) + if (item.poTokens.isNotEmpty()) { + val text = item.poTokens.joinToString("\n") { "PO Token (${it.context}): ${it.token}" } + content.text = text + }else { + content.isVisible = false + } + + val switch = card.findViewById(R.id.materialSwitch) + switch.isChecked = item.enabled + switch.setOnClickListener { + onItemClickListener.onStatusToggle(item, switch.isChecked, position) + } + + //DRAG HANDLE + val dragView = card.findViewById(R.id.drag_view) + dragView.isVisible = showDragHandle + dragView.setOnTouchListener { view, motionEvent -> + view.performClick() + if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN){ + itemTouchHelper.startDrag(holder) + } + true + } + + card.setOnClickListener { + onItemClickListener.onItemClick(item, position) + } + + card.setOnLongClickListener { + onItemClickListener.onDeleteClick(item, position) + true + } + } + + @SuppressLint("NotifyDataSetChanged") + fun toggleShowDragHandle(){ + showDragHandle = !showDragHandle + notifyDataSetChanged() + } + + interface OnItemClickListener { + fun onItemClick(item: YoutubePlayerClientItem, index: Int) + fun onStatusToggle(item: YoutubePlayerClientItem, enabled: Boolean, index: Int) + fun onDeleteClick(item: YoutubePlayerClientItem, index: Int) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: YoutubePlayerClientItem, newItem: YoutubePlayerClientItem): Boolean { + return oldItem.playerClient == newItem.playerClient + } + + override fun areContentsTheSame(oldItem: YoutubePlayerClientItem, newItem: YoutubePlayerClientItem): Boolean { + return oldItem.playerClient == newItem.playerClient && oldItem.enabled == newItem.enabled && oldItem.poTokens.joinToString( + "\n" + ) { it.token } == newItem.poTokens.joinToString( + "\n" + ) { it.token } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 51379c8a..e54e46eb 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -2,13 +2,18 @@ package com.deniscerri.ytdl.ui.downloadcard import android.annotation.SuppressLint import android.app.Dialog -import android.content.* +import android.content.DialogInterface +import android.content.SharedPreferences import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.Window -import android.widget.* +import android.widget.Button +import android.widget.GridLayout +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.core.view.children import androidx.core.view.forEach import androidx.core.view.isVisible @@ -52,8 +57,8 @@ class FormatSelectionBottomSheetDialog( private lateinit var downloadViewModel: DownloadViewModel private lateinit var resultViewModel: ResultViewModel private lateinit var sharedPreferences: SharedPreferences - private lateinit var videoFormatList : LinearLayout - private lateinit var audioFormatList : LinearLayout + private lateinit var videoFormatList : GridLayout + private lateinit var audioFormatList : GridLayout private lateinit var okBtn : Button private lateinit var refreshBtn: Button private lateinit var videoTitle : TextView @@ -80,6 +85,8 @@ class FormatSelectionBottomSheetDialog( private lateinit var genericAudioFormats : List private lateinit var genericVideoFormats : List + private var usingGrid: Boolean = false + enum class FormatSorting { filesize, container, codec, id } @@ -135,6 +142,7 @@ class FormatSelectionBottomSheetDialog( sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) filterBtn = view.findViewById(R.id.format_filter) + usingGrid = sharedPreferences.getBoolean("format_list_grid", false) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) @@ -438,7 +446,32 @@ class FormatSelectionBottomSheetDialog( } + //format layout + val listLayout = filterSheet.findViewById(R.id.layout_list)!! + val gridLayout = filterSheet.findViewById(R.id.layout_grid)!! + if (usingGrid) { + listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) + gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + }else{ + gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) + listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + } + + listLayout.setOnClickListener { + usingGrid = false + gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) + listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + addFormatsToView() + filterSheet.dismiss() + } + gridLayout.setOnClickListener { + usingGrid = true + listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) + gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + addFormatsToView() + filterSheet.dismiss() + } val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) @@ -535,8 +568,6 @@ class FormatSelectionBottomSheetDialog( val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.vcodec.isBlank() || it.vcodec == "none" } != null - videoFormatList.removeAllViews() - audioFormatList.removeAllViews() if (!canMultiSelectAudio) { audioFormatList.visibility = View.GONE @@ -561,6 +592,14 @@ class FormatSelectionBottomSheetDialog( videoFormatList.removeAllViews() audioFormatList.removeAllViews() + if (usingGrid) { + videoFormatList.columnCount = 2 + audioFormatList.columnCount = 2 + }else{ + videoFormatList.columnCount = 1 + audioFormatList.columnCount = 1 + } + if (finalFormats.isEmpty()){ finalFormats = if (items.first()?.type == Type.audio){ genericAudioFormats @@ -571,7 +610,23 @@ class FormatSelectionBottomSheetDialog( for (i in 0.. finalFormats.lastIndex){ val format = finalFormats[i] - val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) + lateinit var formatItem: View + + if (usingGrid){ + formatItem = LayoutInflater.from(context).inflate(R.layout.format_item_grid, null) + formatItem.layoutParams = GridLayout.LayoutParams( + GridLayout.spec(GridLayout.UNDEFINED, 1f), + GridLayout.spec(GridLayout.UNDEFINED, 1f)).apply { + width = 0 + } + }else{ + formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) + formatItem.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, + 1.0f + ) + } formatItem.tag = "${format.format_id}${format.format_note}" UiUtil.populateFormatCard(requireContext(), formatItem as MaterialCardView, format, null) if (selectedVideo == format) formatItem.isChecked = true diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/AdvancedSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/AdvancedSettingsFragment.kt deleted file mode 100644 index aafd5d01..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/AdvancedSettingsFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.deniscerri.ytdl.ui.more.settings - -import android.annotation.SuppressLint -import android.content.DialogInterface -import android.graphics.Typeface -import android.os.Bundle -import android.widget.LinearLayout -import android.widget.TextView -import androidx.preference.EditTextPreference -import androidx.preference.Preference -import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.afollestad.materialdialogs.utils.MDUtil.getStringArray -import com.deniscerri.ytdl.R -import com.deniscerri.ytdl.ui.adapter.SortableTextItemAdapter -import com.deniscerri.ytdl.util.UiUtil -import com.google.android.material.dialog.MaterialAlertDialogBuilder - - -class AdvancedSettingsFragment : BaseSettingsFragment() { - override val title: Int = R.string.advanced - @SuppressLint("RestrictedApi") - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.advanced_preferences, rootKey) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt index 6e4575f0..7d8836aa 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt @@ -334,7 +334,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() { } findPreference("swipe_gesture")?.apply { - val s = getString(R.string.preferred_search_engine_summary) + val s = getString(R.string.swipe_gestures_summary) if (values.size == entries.size) { summary = "${s}\n[${getString(R.string.all)}]" }else if (values.size > 0) { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt new file mode 100644 index 00000000..812401bb --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt @@ -0,0 +1,23 @@ +package com.deniscerri.ytdl.ui.more.settings.advanced + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceManager +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment +import com.deniscerri.ytdl.util.UiUtil + + +class AdvancedSettingsFragment : BaseSettingsFragment() { + override val title: Int = R.string.advanced + @SuppressLint("RestrictedApi") + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.advanced_preferences, rootKey) + findPreference("yt_player_client")?.setOnPreferenceClickListener { + findNavController().navigate(R.id.youtubePlayerClientFragment) + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt new file mode 100644 index 00000000..187cc635 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt @@ -0,0 +1,235 @@ +package com.deniscerri.ytdl.ui.more.settings.advanced + +import android.animation.AnimatorSet +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.RelativeLayout +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.Format +import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem +import com.deniscerri.ytdl.ui.adapter.YoutubePlayerClientAdapter +import com.deniscerri.ytdl.ui.more.settings.SettingsActivity +import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.card.MaterialCardView +import com.google.android.material.chip.Chip +import com.google.common.reflect.TypeToken +import com.google.gson.Gson + + +class YoutubePlayerClientFragment : Fragment(), YoutubePlayerClientAdapter.OnItemClickListener { + private lateinit var recyclerView: RecyclerView + private lateinit var listAdapter: YoutubePlayerClientAdapter + private lateinit var noResults : RelativeLayout + private lateinit var settingsActivity: SettingsActivity + private lateinit var preferences: SharedPreferences + private lateinit var currentList: MutableList + private lateinit var currentListRaw: String + private lateinit var dragHandle: View + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + settingsActivity = activity as SettingsActivity + settingsActivity.changeTopAppbarTitle(getString(R.string.player_client)) + preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + return inflater.inflate(R.layout.fragment_youtube_player_clients, container, false) + } + + @SuppressLint("RestrictedApi") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + noResults = view.findViewById(R.id.no_results) + dragHandle = view.findViewById(R.id.drag) + + val itemTouchHelper = ItemTouchHelper(dragDropHelper) + listAdapter = YoutubePlayerClientAdapter(this,settingsActivity, itemTouchHelper) + recyclerView = view.findViewById(R.id.recycler_view) + recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.adapter = listAdapter + itemTouchHelper.attachToRecyclerView(recyclerView) + currentListRaw = preferences.getString("youtube_player_clients", "[]")!! + val itemType = object : TypeToken>() {}.type + currentList = Gson().fromJson>(currentListRaw, itemType).toMutableList() + listAdapter.submitList(currentList.toList()) + + val newClient = view.findViewById(R.id.newClient) + newClient.setOnClickListener { + UiUtil.showYoutubePlayerClientSheet( + settingsActivity, preferences, null, + newValue = { newItem -> + currentList.add(newItem) + currentListRaw = Gson().toJson(currentList).toString() + updateRecords() + listAdapter.submitList(currentList.toList()) + checkNoResults() + }, + deleted = { + } + ) + } + + checkNoResults() + + dragHandle.setOnClickListener { + listAdapter.toggleShowDragHandle() + } + } + + private fun updateRecords() { + preferences.edit().putString("youtube_player_clients", currentListRaw).apply() + } + + private fun checkNoResults() { + val hasNoResults = currentList.isEmpty() + noResults.isVisible = hasNoResults + recyclerView.isVisible = !hasNoResults + dragHandle.isVisible = !hasNoResults + } + + + override fun onItemClick(item: YoutubePlayerClientItem, index: Int) { + UiUtil.showYoutubePlayerClientSheet( + settingsActivity, preferences, item, + newValue = { newItem -> + currentList.remove(item) + currentList.add(newItem) + currentListRaw = Gson().toJson(currentList).toString() + updateRecords() + listAdapter.submitList(currentList.toList()) + }, + deleted = { + onDeleteClick(item, index) + } + ) + } + + override fun onStatusToggle(item: YoutubePlayerClientItem, enabled: Boolean, index: Int) { + currentList[currentList.indexOf(item)].enabled = enabled + currentListRaw = Gson().toJson(currentList).toString() + updateRecords() + listAdapter.submitList(currentList.toList()) + } + + override fun onDeleteClick(item: YoutubePlayerClientItem, index: Int) { + UiUtil.showGenericDeleteDialog(requireContext(), item.playerClient) { + currentList.remove(item) + currentListRaw = Gson().toJson(currentList).toString() + updateRecords() + listAdapter.submitList(currentList.toList()) + checkNoResults() + } + } + + var movedToNewPositionTag = "" + private val dragDropHelper: ItemTouchHelper.SimpleCallback = + object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val fromPosition = viewHolder.bindingAdapterPosition + val toPosition = target.bindingAdapterPosition + movedToNewPositionTag = target.itemView.tag.toString() + listAdapter.notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) + if (ItemTouchHelper.ACTION_STATE_DRAG == actionState) { + /** + * Change alpha, scale and elevation on drag. + */ + (viewHolder?.itemView as? MaterialCardView)?.also { + AnimatorSet().apply { + this.duration = 100L + this.interpolator = AccelerateDecelerateInterpolator() + + playTogether( + UiUtil.getAlphaAnimator(it, 0.7f), + UiUtil.getScaleXAnimator(it, 1.02f), + UiUtil.getScaleYAnimator(it, 1.02f), + UiUtil.getElevationAnimator(it, R.dimen.elevation_6dp) + ) + }.start() + } + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + /** + * Clear alpha, scale and elevation after drag/swipe + */ + (viewHolder.itemView as? MaterialCardView)?.also { + AnimatorSet().apply { + this.duration = 100L + this.interpolator = AccelerateDecelerateInterpolator() + + playTogether( + UiUtil.getAlphaAnimator(it, 1f), + UiUtil.getScaleXAnimator(it, 1f), + UiUtil.getScaleYAnimator(it, 1f), + UiUtil.getElevationAnimator(it, R.dimen.elevation_2dp) + ) + }.start() + } + + val currentIndex = currentList.indexOfFirst { it.playerClient == viewHolder.itemView.tag.toString() } + val newIndex = currentList.indexOfFirst { it.playerClient == movedToNewPositionTag } + + val newList = mutableListOf() + + if (currentIndex != newIndex && currentIndex >= 0 && newIndex >= 0) { + Log.e("AAAAAAAAAAAAA", "${currentIndex} ${newIndex}") + if(currentIndex > newIndex) { + newList.addAll(currentList.subList(0, newIndex)) + newList.add(currentList[currentIndex]) + currentList.remove(currentList[currentIndex]) + newList.addAll(currentList.subList(newIndex, currentList.size)) + }else{ + val toMove = currentList[currentIndex] + newList.addAll(currentList.subList(0, currentIndex)) + newList.addAll(currentList.subList(currentIndex + 1, newIndex + 1)) + newList.add(toMove) + newList.addAll(currentList.subList(newIndex + 1, currentList.size)) + } + + currentList.clear() + currentList.addAll(newList) + currentListRaw = Gson().toJson(currentList).toString() + updateRecords() + listAdapter.submitList(currentList.toList()) + listAdapter.notifyDataSetChanged() + } + + } + + override fun isLongPressDragEnabled(): Boolean { + return false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 9d73fa6c..f0434385 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -15,6 +15,7 @@ import android.content.SharedPreferences import android.net.Uri import android.os.Build import android.text.Editable +import android.text.InputType import android.text.TextWatcher import android.text.format.DateFormat import android.util.DisplayMetrics @@ -46,6 +47,8 @@ import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml import androidx.core.view.children import androidx.core.view.isVisible +import androidx.core.view.marginBottom +import androidx.core.view.updateLayoutParams import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doOnTextChanged import androidx.fragment.app.FragmentManager @@ -60,6 +63,8 @@ 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.models.YoutubePlayerClientItem +import com.deniscerri.ytdl.database.models.YoutubePoTokenItem import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel @@ -86,10 +91,13 @@ 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.TextInputEditText import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM 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.google.gson.Gson import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -2284,6 +2292,103 @@ object UiUtil { parentView.addView(child) } + bottomSheet.show() + bottomSheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheet.window!!.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + fun showYoutubePlayerClientSheet(context: Activity, preferences: SharedPreferences, currentValue: YoutubePlayerClientItem?, newValue: (item: YoutubePlayerClientItem) -> Unit, deleted: () -> Unit){ + val bottomSheet = BottomSheetDialog(context) + bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) + bottomSheet.setContentView(R.layout.youtube_player_clients) + + val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!! + val chipGroup : ChipGroup = bottomSheet.findViewById(R.id.chipGroup)!! + val suggestedLabel : View = bottomSheet.findViewById(R.id.suggestedLabel)!! + val okBtn : Button = bottomSheet.findViewById(R.id.client_create)!! + val deleteBtn : Button = bottomSheet.findViewById(R.id.client_delete)!! + deleteBtn.isVisible = currentValue != null + + val contentLinear : LinearLayout = bottomSheet.findViewById(R.id.contentLinear)!! + + val defaultChips = context.getStringArray(R.array.youtube_player_clients).toMutableSet() + + 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?) { + chipGroup.children.forEach { (it as Chip).isChecked = false } + chipGroup.children.firstOrNull { (it as Chip).text == p0.toString() }?.apply { + (this as Chip).isChecked = true + } + } + }) + + val existingConfigsRaw = preferences.getString("youtube_player_clients", "[]") + val itemType = object : com.google.common.reflect.TypeToken>() {}.type + val existingConfigs = Gson().fromJson>(existingConfigsRaw, itemType).toMutableList() + + defaultChips.filter { it.isNotBlank() }.forEach { + if (!existingConfigs.any { it2 -> it2.playerClient == it }) { + val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip + tmp.text = it + tmp.setOnClickListener { + title.editText!!.setText(tmp.text.toString()) + } + chipGroup.addView(tmp) + } + } + + if (chipGroup.children.count() == 0) { + suggestedLabel.isVisible = false + } + + val poTokenInputs = contentLinear.children.filter { it is TextInputLayout }.map { it as TextInputLayout }.toList() + for(p in poTokenInputs) { + p.setEndIconOnClickListener { + val clipboard: ClipboardManager = context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager + p.editText!!.setText(clipboard.primaryClip?.getItemAt(0)?.text) + } + } + + currentValue?.apply { + title.editText!!.setText(this.playerClient) + for (t in this.poTokens) { + poTokenInputs.firstOrNull { it.tag == "potoken_${t.context}" }?.editText?.setText(t.token) + } + okBtn.text = context.getString(R.string.edit) + } + + okBtn.setOnClickListener { + val titleVal = title.editText!!.text.toString() + if (titleVal.isBlank()) { + title.error = "Player Client tag shouldn't be empty" + return@setOnClickListener + } + + if(existingConfigs.any { it2 -> it2.playerClient == titleVal } && ( currentValue == null || currentValue.playerClient != titleVal )) { + title.error = "Player Client is already created" + return@setOnClickListener + } + + val obj = YoutubePlayerClientItem(titleVal, mutableListOf()) + poTokenInputs.filter { it.editText!!.text.isNotBlank() && it.tag.toString().startsWith("potoken") }.forEach { + obj.poTokens.add(YoutubePoTokenItem(it.tag.toString().split("potoken_")[1], it.editText!!.text.toString())) + } + bottomSheet.cancel() + newValue(obj) + } + + deleteBtn.setOnClickListener { + bottomSheet.cancel() + deleted() + } + + bottomSheet.show() bottomSheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED bottomSheet.window!!.setLayout( diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt index e3b8ebc1..f017809e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt @@ -16,6 +16,7 @@ import com.deniscerri.ytdl.database.models.ChapterItem import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.ResultItem +import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.Extensions.getIntByAny @@ -1237,27 +1238,40 @@ class YTDLPUtil(private val context: Context) { } private fun YoutubeDLRequest.setYoutubeExtractorArgs() { - val playerClient = sharedPreferences.getString("yt_player_client", "")!!.split(",").filter { it.isNotBlank() }.toMutableList() val extractorArgs = mutableListOf() + val playerClients = mutableListOf() + val poTokens = mutableListOf() - val poToken = sharedPreferences.getString("youtube_po_token", "")!! - if (poToken.isNotBlank() && !playerClient.contains("web")) { - playerClient.add("web") + val configuredPlayerClientsRaw = sharedPreferences.getString("youtube_player_clients", "[]")!! + val itemType = object : com.google.common.reflect.TypeToken>() {}.type + val configuredPlayerClients = Gson().fromJson>(configuredPlayerClientsRaw, itemType).toMutableList() + + for (value in configuredPlayerClients) { + if (value.enabled) { + playerClients.add(value.playerClient) + value.poTokens.forEach { pt -> + poTokens.add("${value.playerClient}.${pt.context}+${pt.token}") + } + } } - if (playerClient.isNotEmpty()){ - extractorArgs.add("player_client=${playerClient.joinToString(",")}") + if (playerClients.isNotEmpty()){ + extractorArgs.add("player_client=${playerClients.joinToString(",")}") } - val lang = Locale.getDefault().language - val langTag = Locale.getDefault().toLanguageTag() - if (context.getStringArray(R.array.subtitle_langs).contains(lang)) { - extractorArgs.add("lang=$lang") - }else if (context.getStringArray(R.array.subtitle_langs).contains(langTag)) { - extractorArgs.add("lang=$langTag") + if (poTokens.isNotEmpty()) { + extractorArgs.add("po_token=${poTokens.joinToString(",")}") } - if (poToken.isNotBlank()) { - extractorArgs.add("po_token=web+$poToken") + + val useLanguageForMetadata = sharedPreferences.getBoolean("use_app_language_for_metadata", true) + if (useLanguageForMetadata) { + val lang = Locale.getDefault().language + val langTag = Locale.getDefault().toLanguageTag() + if (context.getStringArray(R.array.subtitle_langs).contains(lang)) { + extractorArgs.add("lang=$lang") + }else if (context.getStringArray(R.array.subtitle_langs).contains(langTag)) { + extractorArgs.add("lang=$langTag") + } } val otherArgs = sharedPreferences.getString("youtube_other_extractor_args", "")!! diff --git a/app/src/main/res/drawable/baseline_person_24.xml b/app/src/main/res/drawable/baseline_person_24.xml new file mode 100644 index 00000000..a301d55f --- /dev/null +++ b/app/src/main/res/drawable/baseline_person_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/active_download_card.xml b/app/src/main/res/layout/active_download_card.xml index 7a7c08d7..63ef7d17 100644 --- a/app/src/main/res/layout/active_download_card.xml +++ b/app/src/main/res/layout/active_download_card.xml @@ -110,8 +110,9 @@ app:layout_constraintBottom_toTopOf="@+id/output" app:layout_constraintEnd_toStartOf="@+id/active_pause_cancel_btns" app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintVertical_bias="0.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"> + app:layout_constraintTop_toTopOf="@+id/active_pause_cancel_btns"> - - - diff --git a/app/src/main/res/layout/format_category_sheet.xml b/app/src/main/res/layout/format_category_sheet.xml index 82ea9d5c..6ccd9830 100644 --- a/app/src/main/res/layout/format_category_sheet.xml +++ b/app/src/main/res/layout/format_category_sheet.xml @@ -83,7 +83,6 @@ - + + + + + + + + + + + + diff --git a/app/src/main/res/layout/format_item.xml b/app/src/main/res/layout/format_item.xml index 78541700..f39f6f5d 100644 --- a/app/src/main/res/layout/format_item.xml +++ b/app/src/main/res/layout/format_item.xml @@ -22,11 +22,12 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/format_select_bottom_sheet.xml b/app/src/main/res/layout/format_select_bottom_sheet.xml index abf82f04..949d4791 100644 --- a/app/src/main/res/layout/format_select_bottom_sheet.xml +++ b/app/src/main/res/layout/format_select_bottom_sheet.xml @@ -134,11 +134,12 @@ android:paddingVertical="20dp" android:text="@string/video" /> - + android:columnCount="1" + android:alignmentMode="alignBounds" + android:layout_height="wrap_content" /> - + android:columnCount="1" + android:alignmentMode="alignBounds" + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/fragment_youtube_player_clients.xml b/app/src/main/res/layout/fragment_youtube_player_clients.xml new file mode 100644 index 00000000..1fc730a0 --- /dev/null +++ b/app/src/main/res/layout/fragment_youtube_player_clients.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_client_item.xml b/app/src/main/res/layout/player_client_item.xml new file mode 100644 index 00000000..4fc17c42 --- /dev/null +++ b/app/src/main/res/layout/player_client_item.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/youtube_player_clients.xml b/app/src/main/res/layout/youtube_player_clients.xml new file mode 100644 index 00000000..8049c17f --- /dev/null +++ b/app/src/main/res/layout/youtube_player_clients.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + +