fixed webview ok button

slightly improved listing of items if u have alot of items
fixed app not enabling ok button in the cut screen if you change the TO text box only
added force keyframes at cuts switch in the cut screen
Added 3 dots in multiple download card. It will have the configure chips in a separate card. Each will have a callback that will update all items in multiple download
fixed start end textboxes showing truncanted text in playlist select card
add ability to start a download now and put the rest of the queue behind it
Scrollbar handle shrinked depending on size. Made it same size
Fixed Tapping the notification of the errored download leading to the running tab instead of errored
Set different icon for terminal in share menu
ignored extra command preference in backups of command templates
made preference titles be multiline if they are too long
if the preferred download type is command, the app will not bother fetching data beforehand
fixed app not showing the entire interface when being in landscape
hid format details in command type logs because they are useless
fixed cookies import not working from external sources other than the app
fixed app using continue button for a split second when there are no paused downloads
added state in home screen so that it wont populate trending videos while its searching for an item
implemented batch downloads in a single worker to avoid bogging down the system with many works
fixed instagram status that have multiple videos just using the first video
fixed monochrome icon not showing
made yt-dlp auto update a choice in the settings
fixed bilibili not working in normal mode
fixed app overwriting files instead of adding (1) in the filename instead
This commit is contained in:
deniscerri 2023-08-20 17:01:39 +02:00
parent 7191dc00f2
commit e91262cc66
No known key found for this signature in database
GPG key ID: 95C43D517D830350
108 changed files with 3873 additions and 2653 deletions

View file

@ -1,4 +1,4 @@
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="corretto-17" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK" />
</project> </project>

View file

@ -2,15 +2,15 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' id 'com.google.devtools.ksp'
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10"
} }
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 6
def versionPatch = 3 def versionPatch = 5
def versionBuild = 17 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""
if (versionBuild > 0){ if (versionBuild > 0){
@ -49,10 +49,8 @@ android {
useSupportLibrary true useSupportLibrary true
} }
kapt { ksp {
arguments { arg("room.schemaLocation", "$projectDir/schemas")
arg("room.schemaLocation", "$projectDir/schemas")
}
} }
} }
@ -76,6 +74,8 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false debuggable false
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
applicationIdSuffix '.beta'
versionNameSuffix '-Beta'
} }
debug { debug {
@ -98,7 +98,7 @@ android {
disable 'MissingTranslation' disable 'MissingTranslation'
} }
android.applicationVariants.all { variant -> android.applicationVariants.configureEach { variant ->
variant.outputs.each { output -> variant.outputs.each { output ->
def baseAbiVersionCode = project.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) def baseAbiVersionCode = project.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
if (baseAbiVersionCode != null) { if (baseAbiVersionCode != null) {
@ -149,7 +149,7 @@ dependencies {
implementation 'com.google.android.material:material:1.9.0-rc01' implementation 'com.google.android.material:material:1.9.0-rc01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "androidx.navigation:navigation-fragment-ktx:$navVer" implementation "androidx.navigation:navigation-fragment-ktx:$navVer"
implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
@ -174,16 +174,18 @@ dependencies {
implementation "androidx.room:room-runtime:2.5.2" implementation "androidx.room:room-runtime:2.5.2"
implementation "androidx.room:room-ktx:2.5.2" implementation "androidx.room:room-ktx:2.5.2"
kapt "androidx.room:room-compiler:2.5.2" ksp "androidx.room:room-compiler:2.5.2"
implementation 'androidx.paging:paging-runtime-ktx:3.2.0'
implementation "androidx.room:room-paging:2.5.2"
androidTestImplementation "androidx.room:room-testing:2.5.2" androidTestImplementation "androidx.room:room-testing:2.5.2"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation "com.google.truth:truth:1.1.3" androidTestImplementation 'com.google.truth:truth:1.1.5'
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer"
implementation 'com.google.code.gson:gson:2.9.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10" implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
@ -200,7 +202,7 @@ dependencies {
implementation("androidx.media3:media3-ui:$media3_version") implementation("androidx.media3:media3-ui:$media3_version")
// For RTSP playback support with ExoPlayer // For RTSP playback support with ExoPlayer
implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version") implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version")
implementation("androidx.media3:media3-datasource-cronet:$media3_version")
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
} }

View file

@ -57,6 +57,17 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:mimeType="video/*" />
<data android:mimeType="audio/*" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name=".receiver.ShareFileActivity" android:name=".receiver.ShareFileActivity"
@ -96,6 +107,8 @@
android:name=".terminalShareAlias" android:name=".terminalShareAlias"
android:targetActivity=".ui.more.TerminalActivity" android:targetActivity=".ui.more.TerminalActivity"
android:enabled="false" android:enabled="false"
android:icon="@mipmap/terminal_icon"
android:roundIcon="@mipmap/terminal_icon_round"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@ -204,7 +217,7 @@
</service> </service>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="com.deniscerri.ytdl.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:enabled="true" android:enabled="true"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
@ -212,18 +225,6 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application> </application>
</manifest> </manifest>

View file

@ -9,14 +9,11 @@ import androidx.work.Configuration
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.deniscerri.ytdlnis.work.UpdateYTDLWorker import com.deniscerri.ytdlnis.work.UpdateYTDLWorker
import com.google.android.gms.common.internal.Constants
import com.yausername.aria2c.Aria2c import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -50,21 +47,27 @@ class App : Application() {
} }
} }
//init yt-dlp auto update with work request if (sharedPreferences.getBoolean("auto_update_ytdlp", true)){
val constraints = Constraints.Builder() //init yt-dlp auto update with work request
.setRequiredNetworkType(NetworkType.UNMETERED) val constraints = Constraints.Builder()
.setRequiresDeviceIdle(true) .setRequiredNetworkType(NetworkType.UNMETERED)
.build() .setRequiresDeviceIdle(true)
.build()
val periodicWorkRequest = PeriodicWorkRequestBuilder<UpdateYTDLWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(this@App).enqueueUniquePeriodicWork(
"ytdlp-Updater",
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest
)
}else{
WorkManager.getInstance(this@App).cancelUniqueWork("ytdlp-Updater")
}
val periodicWorkRequest = PeriodicWorkRequestBuilder<UpdateYTDLWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(this@App).enqueueUniquePeriodicWork(
"ytdlp-Updater",
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest
)
}catch (e: Exception){ }catch (e: Exception){
Looper.prepare().runCatching { Looper.prepare().runCatching {
@ -73,14 +76,6 @@ class App : Application() {
e.printStackTrace() e.printStackTrace()
} }
} }
WorkManager.initialize(
this@App,
Configuration.Builder()
.setExecutor(Executors.newFixedThreadPool(
sharedPreferences.getInt("concurrent_downloads", 1)))
.build())
} }
@Throws(YoutubeDLException::class) @Throws(YoutubeDLException::class)
private fun initLibraries() { private fun initLibraries() {

View file

@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.graphics.Color import android.graphics.Color
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -106,14 +107,14 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
} }
val formatDetailsChip = card.findViewById<Chip>(R.id.format_note) val formatDetailsChip = card.findViewById<Chip>(R.id.format_note)
val formatDetailsText = StringBuilder(item.format.format_note.uppercase())
formatDetailsText.append(" \t\t ${item.container.uppercase().ifEmpty { item.format.container.uppercase() }}") val sideDetails = mutableListOf<String>()
sideDetails.add(item.format.format_note.uppercase())
sideDetails.add(item.container.uppercase().ifEmpty { item.format.container.uppercase() })
val fileSize = FileUtil.convertFileSize(item.format.filesize) val fileSize = FileUtil.convertFileSize(item.format.filesize)
if (fileSize != "?") formatDetailsText.append(" \t\t $fileSize") if (fileSize != "?") sideDetails.add(fileSize)
formatDetailsChip.text = sideDetails.joinToString(" · ")
formatDetailsChip.text = formatDetailsText
//OUTPUT //OUTPUT
val output = card.findViewById<TextView>(R.id.output) val output = card.findViewById<TextView>(R.id.output)

View file

@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.adapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
@ -11,10 +10,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -24,16 +23,18 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, GenericDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : PagingDataAdapter<DownloadItem, GenericDownloadAdapter.ViewHolder>(DIFF_CALLBACK) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private val checkedItems: ArrayList<Long> val checkedItems: ArrayList<Long>
var inverted: Boolean
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
init { init {
checkedItems = ArrayList() checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener this.onItemClickListener = onItemClickListener
this.activity = activity this.activity = activity
this.inverted = false
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
} }
@ -53,13 +54,16 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
card.isVisible = item != null
if (item == null) return
card.tag = item.id.toString()
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view) val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){ if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){
val imageURL = item!!.thumb val imageURL = item.thumb
if (imageURL.isNotEmpty()) { if (imageURL.isNotEmpty()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else { } else {
@ -127,7 +131,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
actionButton.setOnClickListener { actionButton.setOnClickListener {
onItemClickListener.onActionButtonClick(item.id) onItemClickListener.onActionButtonClick(item.id)
} }
if (checkedItems.contains(item.id)) { if ((checkedItems.contains(item.id) && !inverted) || (!checkedItems.contains(item.id) && inverted)) {
card.isChecked = true card.isChecked = true
card.strokeWidth = 5 card.strokeWidth = 5
} else { } else {
@ -135,67 +139,69 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
card.strokeWidth = 0 card.strokeWidth = 0
} }
card.setOnClickListener { card.setOnClickListener {
if (checkedItems.size > 0) { if (checkedItems.size > 0 || inverted) {
checkCard(card, item.id) checkCard(card, item.id, position)
} else { } else {
onItemClickListener.onCardClick(item.id) onItemClickListener.onCardClick(item.id)
} }
} }
card.setOnLongClickListener { card.setOnLongClickListener {
checkCard(card, item.id) checkCard(card, item.id, position)
true true
} }
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun clearCheckeditems() { fun clearCheckedItems() {
for (i in 0 until itemCount){ inverted = false
val item = getItem(i)
if (checkedItems.find { it == item?.id } != null){
checkedItems.remove(item?.id)
notifyItemChanged(i)
}
}
checkedItems.clear() checkedItems.clear()
}
@SuppressLint("NotifyDataSetChanged")
fun checkAll(items: List<DownloadItem?>?){
checkedItems.clear()
checkedItems.addAll(items!!.map { it!!.id })
notifyDataSetChanged() notifyDataSetChanged()
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun invertSelected(items: List<DownloadItem?>?){ fun checkAll() {
val invertedList = mutableListOf<Long>()
items?.forEach {
if (!checkedItems.contains(it!!.id)) invertedList.add(it.id)
}
checkedItems.clear() checkedItems.clear()
checkedItems.addAll(invertedList) inverted = true
notifyDataSetChanged() notifyDataSetChanged()
} }
private fun checkCard(card: MaterialCardView, itemID: Long) { @SuppressLint("NotifyDataSetChanged")
fun invertSelected() {
inverted = !inverted
notifyDataSetChanged()
}
fun getSelectedObjectsCount(totalSize: Int) : Int{
return if (inverted){
totalSize - checkedItems.size
}else{
checkedItems.size
}
}
private fun checkCard(card: MaterialCardView, itemID: Long, position: Int) {
if (card.isChecked) { if (card.isChecked) {
card.strokeWidth = 0 card.strokeWidth = 0
checkedItems.remove(itemID) if (inverted) checkedItems.add(itemID)
else checkedItems.remove(itemID)
} else { } else {
card.strokeWidth = 5 card.strokeWidth = 5
checkedItems.add(itemID) if (inverted) checkedItems.remove(itemID)
else checkedItems.add(itemID)
} }
card.isChecked = !card.isChecked card.isChecked = !card.isChecked
onItemClickListener.onCardSelect(itemID, card.isChecked) onItemClickListener.onCardSelect(card.isChecked, position)
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onActionButtonClick(itemID: Long) fun onActionButtonClick(itemID: Long)
fun onCardClick(itemID: Long) fun onCardClick(itemID: Long)
fun onCardSelect(itemID: Long, isChecked: Boolean) fun onCardSelect(isChecked: Boolean, position: Int)
} }
companion object { companion object {

View file

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import org.w3c.dom.Text
import java.util.* import java.util.*
@ -70,17 +71,9 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) } uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
} }
// TITLE ---------------------------------- card.findViewById<TextView>(R.id.title).text = item!!.title
val itemTitle = card.findViewById<TextView>(R.id.title) card.findViewById<TextView>(R.id.author).text = item.author
var title = item!!.title card.findViewById<TextView>(R.id.duration).text = item.duration
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}
itemTitle.text = title
val duration = card.findViewById<TextView>(R.id.duration)
duration.text = item.duration
// CHECKBOX ---------------------------------- // CHECKBOX ----------------------------------
val check = card.findViewById<CheckBox>(R.id.checkBox) val check = card.findViewById<CheckBox>(R.id.checkBox)

View file

@ -13,16 +13,9 @@ import java.lang.reflect.Type
class Converters { class Converters {
@TypeConverter @TypeConverter
fun stringToListOfFormats(value: String?): ArrayList<Format> { fun stringToListOfFormats(value: String) = Gson().fromJson(value, Array<Format>::class.java).toMutableList()
val listType: Type = object : TypeToken<ArrayList<Format?>?>() {}.type
return Gson().fromJson(value, listType)
}
@TypeConverter @TypeConverter
fun listOfFormatsToString(list: ArrayList<Format?>?): String { fun listOfFormatsToString(list: List<Format?>?) = Gson().toJson(list).toString()
val gson = Gson()
return gson.toJson(list)
}
@TypeConverter @TypeConverter
fun formatToString(format: Format): String = Gson().toJson(format) fun formatToString(format: Format): String = Gson().toJson(format)
@ -53,13 +46,10 @@ class Converters {
fun stringToVideoPreferences(string: String): VideoPreferences = Gson().fromJson(string, VideoPreferences::class.java) fun stringToVideoPreferences(string: String): VideoPreferences = Gson().fromJson(string, VideoPreferences::class.java)
@TypeConverter @TypeConverter
fun stringToListOfChapters(value: String?): ArrayList<ChapterItem>? { fun stringToListOfChapters(value: String?) = Gson().fromJson(value, Array<ChapterItem>::class.java).toMutableList()
val listType: Type = object : TypeToken<ArrayList<ChapterItem?>?>() {}.type
return Gson().fromJson(value, listType)
}
@TypeConverter @TypeConverter
fun listOfChaptersToString(list: ArrayList<ChapterItem?>?): String { fun listOfChaptersToString(list: List<ChapterItem?>?): String {
val gson = Gson() val gson = Gson()
return gson.toJson(list) return gson.toJson(list)
} }

View file

@ -1,9 +1,14 @@
package com.deniscerri.ytdlnis.database package com.deniscerri.ytdlnis.database
import android.content.Context import android.content.Context
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.dao.* import com.deniscerri.ytdlnis.database.dao.*
import com.deniscerri.ytdlnis.database.models.* import com.deniscerri.ytdlnis.database.models.*
import kotlinx.coroutines.flow.Flow
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@Database( @Database(

View file

@ -1,20 +1,26 @@
package com.deniscerri.ytdlnis.database.dao package com.deniscerri.ytdlnis.database.dao
import androidx.paging.PagingSource
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface DownloadDao { interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status") @Query("SELECT * FROM downloads ORDER BY status")
fun getAllDownloads() : Flow<List<DownloadItem>> fun getAllDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'")
fun getActiveDownloads() : Flow<List<DownloadItem>> fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT COUNT(*) FROM downloads WHERE status='Active' or status='Paused'") @Query("SELECT COUNT(*) FROM downloads WHERE status='Active' or status='Paused'")
fun getActiveDownloadsCount() : Flow<Int> fun getActiveDownloadsCountFlow() : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatus(status : List<String>) : Flow<Int>
@Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'")
fun getActiveDownloadsList() : List<DownloadItem> fun getActiveDownloadsList() : List<DownloadItem>
@ -22,26 +28,41 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'")
fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id")
fun getQueuedDownloads() : Flow<List<DownloadItem>> fun getQueuedDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT format FROM downloads WHERE status='Queued' or status='QueuedPaused'")
fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status='Queued' ORDER BY downloadStartTime, id LIMIT 20")
fun getQueuedDownloadsThatAreNotScheduledChunked(currentTime: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' or status='Paused' ORDER BY downloadStartTime, id")
fun getQueuedAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem> fun getQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloads() : Flow<List<DownloadItem>> fun getCancelledDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloadsList() : List<DownloadItem> fun getCancelledDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : Flow<List<DownloadItem>> fun getErroredDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem> fun getErroredDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : Flow<List<DownloadItem>> fun getSavedDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem fun getDownloadById(id: Long) : DownloadItem
@ -52,6 +73,22 @@ interface DownloadDao {
@Query("SELECT status FROM downloads WHERE id=:id") @Query("SELECT status FROM downloads WHERE id=:id")
fun checkStatus(id: Long) : String fun checkStatus(id: Long) : String
@Query("UPDATE downloads " +
"SET status = CASE " +
" WHEN status = 'Active' THEN 'Paused' " +
" WHEN status = 'Queued' THEN 'QueuedPaused' " +
" ELSE status " +
" END;")
fun pauseActiveAndQueued()
@Query("UPDATE downloads " +
"SET status = CASE " +
" WHEN status = 'Paused' THEN 'Active' " +
" WHEN status = 'QueuedPaused' THEN 'Queued' " +
" ELSE status " +
" END;")
fun unPauseActiveAndQueued()
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: DownloadItem) : Long suspend fun insert(item: DownloadItem) : Long
@ -73,7 +110,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Saved'") @Query("DELETE FROM downloads WHERE status='Saved'")
suspend fun deleteSaved() suspend fun deleteSaved()
@Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued'") @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused'")
suspend fun cancelQueued() suspend fun cancelQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -82,6 +119,9 @@ interface DownloadDao {
@Upsert @Upsert
suspend fun update(item: DownloadItem) suspend fun update(item: DownloadItem)
@Upsert
fun updateMultiple(items :List<DownloadItem>)
@Query("SELECT * FROM downloads ORDER BY id DESC LIMIT 1") @Query("SELECT * FROM downloads ORDER BY id DESC LIMIT 1")
fun getLatest() : DownloadItem fun getLatest() : DownloadItem
@ -94,4 +134,38 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE url=:url AND format=:format AND (status='Error' OR status='Cancelled') LIMIT 1") @Query("SELECT * FROM downloads WHERE url=:url AND format=:format AND (status='Error' OR status='Cancelled') LIMIT 1")
fun getUnfinishedByURLAndFormat(url: String, format: String) : DownloadItem fun getUnfinishedByURLAndFormat(url: String, format: String) : DownloadItem
@Query("SELECT COUNT(downloadStartTime) = 0 from downloads WHERE id in (:items) AND " +
"CASE :inverted " +
"WHEN :inverted = 'false' THEN downloadStartTime > :currentStartTime " +
"WHEN :inverted = 'true' THEN downloadStartTime < :currentStartTime ELSE downloadStartTime < -1 END")
fun checkAllQueuedItemsAreScheduledAfterNow(items: List<Long>, inverted: String, currentStartTime: Long) : Boolean
@Query("Select id from downloads where id not in (:list) and status in (:status)")
fun getDownloadIDsNotPresentInList(list: List<Long>, status: List<String>) : List<Long>
@Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)")
fun resetScheduleTimeForItems(list: List<Long>)
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
fun reQueueDownloadItems(list: List<Long>)
@Transaction
fun putAtTopOfTheQueue(id: Long){
val downloads = getQueuedDownloadsList()
val newID = downloads.first().id
updateDownloadID(id, -id)
resetScheduleTimeForItems(listOf(-id))
downloads.reversed().dropWhile { it.id == id }.forEach {
updateDownloadID(it.id, it.id + 1)
}
updateDownloadID(-id, newID)
}
@Query("Update downloads set id=:newId where id=:id")
fun updateDownloadID(id: Long, newId: Long)
} }

View file

@ -12,7 +12,7 @@ interface SearchHistoryDao {
fun getAllByKeyword(keyword: String) : List<SearchHistoryItem> fun getAllByKeyword(keyword: String) : List<SearchHistoryItem>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(new: SearchHistoryItem) suspend fun insert(newItem: SearchHistoryItem)
@Query("DELETE FROM searchHistory") @Query("DELETE FROM searchHistory")
suspend fun deleteAll() suspend fun deleteAll()

View file

@ -20,7 +20,7 @@ data class DownloadItem(
var container: String, var container: String,
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
var downloadSections: String, var downloadSections: String,
val allFormats: ArrayList<Format>, val allFormats: MutableList<Format>,
var downloadPath: String, var downloadPath: String,
var website: String, var website: String,
val downloadSize: String, val downloadSize: String,

View file

@ -15,9 +15,9 @@ data class ResultItem(
val thumb: String, val thumb: String,
val website: String, val website: String,
var playlistTitle: String, var playlistTitle: String,
var formats: ArrayList<Format>, var formats: MutableList<Format>,
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
var urls: String, var urls: String,
var chapters: ArrayList<ChapterItem>?, var chapters: MutableList<ChapterItem>?,
var creationTime: Long = System.currentTimeMillis() / 1000, var creationTime: Long = System.currentTimeMillis() / 1000,
) )

View file

@ -1,19 +1,36 @@
package com.deniscerri.ytdlnis.database.repository package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData import androidx.paging.Pager
import com.deniscerri.ytdlnis.database.Converters import androidx.paging.PagingConfig
import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class DownloadRepository(private val downloadDao: DownloadDao) { class DownloadRepository(private val downloadDao: DownloadDao) {
val allDownloads : Flow<List<DownloadItem>> = downloadDao.getAllDownloads() val allDownloads : Pager<Int, DownloadItem> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getAllDownloads()}
)
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads() val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads()
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCount() val queuedDownloads : Pager<Int, DownloadItem> = Pager(
val queuedDownloads : Flow<List<DownloadItem>> = downloadDao.getQueuedDownloads() config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
val cancelledDownloads : Flow<List<DownloadItem>> = downloadDao.getCancelledDownloads() pagingSourceFactory = {downloadDao.getQueuedDownloads()}
val erroredDownloads : Flow<List<DownloadItem>> = downloadDao.getErroredDownloads() )
val savedDownloads : Flow<List<DownloadItem>> = downloadDao.getSavedDownloads() val cancelledDownloads : Pager<Int, DownloadItem> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getCancelledDownloads()}
)
val erroredDownloads : Pager<Int, DownloadItem> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getErroredDownloads()}
)
val savedDownloads : Pager<Int, DownloadItem> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getSavedDownloads()}
)
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCountFlow()
enum class Status { enum class Status {
Active, Paused, Queued, QueuedPaused, Error, Cancelled, Saved Active, Paused, Queued, QueuedPaused, Error, Cancelled, Saved
@ -27,8 +44,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.insertAll(items) return downloadDao.insertAll(items)
} }
suspend fun delete(item: DownloadItem){ suspend fun delete(id: Long){
downloadDao.delete(item.id) downloadDao.delete(id)
} }
suspend fun update(item: DownloadItem){ suspend fun update(item: DownloadItem){
@ -61,6 +78,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getCancelledDownloadsList() return downloadDao.getCancelledDownloadsList()
} }
fun getPausedDownloads() : List<DownloadItem> {
return downloadDao.getPausedDownloadsList()
}
fun getErroredDownloads() : List<DownloadItem> { fun getErroredDownloads() : List<DownloadItem> {
return downloadDao.getErroredDownloadsList() return downloadDao.getErroredDownloadsList()
} }
@ -81,20 +102,12 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.cancelQueued() downloadDao.cancelQueued()
} }
fun getLastDownloadId() : Long { fun pauseDownloads(){
return downloadDao.getLastDownloadId() downloadDao.pauseActiveAndQueued()
} }
fun unPauseDownloads(){
fun checkIfReDownloadingErroredOrCancelled(item: DownloadItem) : Long { downloadDao.unPauseActiveAndQueued()
val converters = Converters()
val format = converters.formatToString(item.format)
return try {
val i = downloadDao.getUnfinishedByURLAndFormat(item.url, format)
i.id
}catch (e: Exception){
0L
}
} }
} }

View file

@ -12,23 +12,24 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.anggrayudi.storage.file.dataDirectory import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.database.repository.CookieRepository
import com.deniscerri.ytdlnis.ui.more.WebViewActivity import com.deniscerri.ytdlnis.ui.more.WebViewActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.util.Date
class CookieViewModel(private val application: Application) : AndroidViewModel(application) { class CookieViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: CookieRepository private val repository: CookieRepository
val items: LiveData<List<CookieItem>> val items: LiveData<List<CookieItem>>
private val jsonFormat = Json { prettyPrint = true }
private val cookieHeader = "# Netscape HTTP Cookie File\n" + private val cookieHeader =
"# WebView Generated by the YTDLnis app\n" "# Netscape HTTP Cookie File\n" +
"# WebView Generated by the YTDLnis app\n" +
"# This is a generated file! Do not edit."
init { init {
val dao = DBManager.getInstance(application).cookieDao val dao = DBManager.getInstance(application).cookieDao
@ -72,8 +73,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
if (!hasCookies()) throw Exception("There is no cookies in the database!") if (!hasCookies()) throw Exception("There is no cookies in the database!")
flush() flush()
} }
val dbPath = File("/data/data/${BuildConfig.APPLICATION_ID}/").walkTopDown().find { it.name == "Cookies" }
val dbPath = File("/data/data/com.deniscerri.ytdl/").walkTopDown().find { it.name == "Cookies" }
?: throw Exception("Cookies File not found!") ?: throw Exception("Cookies File not found!")
SQLiteDatabase.openDatabase( SQLiteDatabase.openDatabase(
@ -139,11 +139,12 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
var clip = clipboard.primaryClip!!.getItemAt(0).text var clip = clipboard.primaryClip!!.getItemAt(0).text
Log.e("Aaa", clip.toString()) Log.e("Aaa", clip.toString())
if (clip.startsWith(cookieHeader)){
if (clip.startsWith("# Netscape HTTP Cookie File")){
clip = clip.removePrefix(cookieHeader) clip = clip.removePrefix(cookieHeader)
val cookie = CookieItem( val cookie = CookieItem(
0, 0,
"Cookie Import [${System.currentTimeMillis()}]", "Cookie Import at [${Date()}]",
clip.toString() clip.toString()
) )
insert(cookie) insert(cookie)

View file

@ -8,22 +8,20 @@ import android.content.res.Resources
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.Looper import android.os.Looper
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkRequest import androidx.work.await
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
@ -44,18 +42,12 @@ import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlin.random.nextInt
class DownloadViewModel(application: Application) : AndroidViewModel(application) { class DownloadViewModel(application: Application) : AndroidViewModel(application) {
@ -64,13 +56,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil private val infoUtil : InfoUtil
val allDownloads : LiveData<List<DownloadItem>> val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : LiveData<List<DownloadItem>> val queuedDownloads : Flow<PagingData<DownloadItem>>
val activeDownloads : LiveData<List<DownloadItem>> val activeDownloads : LiveData<List<DownloadItem>>
val activeDownloadsCount : LiveData<Int> val activeDownloadsCount : LiveData<Int>
val cancelledDownloads : LiveData<List<DownloadItem>> val cancelledDownloads : Flow<PagingData<DownloadItem>>
val erroredDownloads : LiveData<List<DownloadItem>> val erroredDownloads : Flow<PagingData<DownloadItem>>
val savedDownloads : LiveData<List<DownloadItem>> val savedDownloads : Flow<PagingData<DownloadItem>>
private var bestVideoFormat : Format private var bestVideoFormat : Format
private var bestAudioFormat : Format private var bestAudioFormat : Format
@ -81,6 +73,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private val audioFormatIDPreference: List<String> private val audioFormatIDPreference: List<String>
private val resources : Resources private val resources : Resources
private var extraCommands: String = "" private var extraCommands: String = ""
private var audioContainer: String?
private var videoContainer: String?
private var videoCodec: String?
enum class Type { enum class Type {
audio, video, command audio, video, command
} }
@ -93,13 +90,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application) infoUtil = InfoUtil(application)
allDownloads = repository.allDownloads.asLiveData() allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.asLiveData() queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads.asLiveData() activeDownloads = repository.activeDownloads.asLiveData()
activeDownloadsCount = repository.activeDownloadsCount.asLiveData() activeDownloadsCount = repository.activeDownloadsCount.asLiveData()
savedDownloads = repository.savedDownloads.asLiveData() savedDownloads = repository.savedDownloads.flow
cancelledDownloads = repository.cancelledDownloads.asLiveData() cancelledDownloads = repository.cancelledDownloads.flow
erroredDownloads = repository.erroredDownloads.asLiveData() erroredDownloads = repository.erroredDownloads.flow
viewModelScope.launch(Dispatchers.IO){ viewModelScope.launch(Dispatchers.IO){
if (sharedPreferences.getBoolean("use_extra_commands", false)){ if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands = commandTemplateDao.getAllTemplatesAsExtraCommands().joinToString(" ") extraCommands = commandTemplateDao.getAllTemplatesAsExtraCommands().joinToString(" ")
@ -107,8 +104,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",") formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",") audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val confTmp = Configuration(application.resources.configuration) val confTmp = Configuration(application.resources.configuration)
confTmp.locale = Locale(sharedPreferences.getString("app_language", "en")!!) confTmp.locale = Locale(sharedPreferences.getString("app_language", "en")!!)
@ -117,7 +114,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val videoFormatValues = resources.getStringArray(R.array.video_formats_values) val videoFormatValues = resources.getStringArray(R.array.video_formats_values)
val videoContainer = sharedPreferences.getString("video_format", "Default") videoContainer = sharedPreferences.getString("video_format", "Default")
defaultVideoFormats = mutableListOf() defaultVideoFormats = mutableListOf()
videoFormatValues.forEach { videoFormatValues.forEach {
@ -132,29 +129,55 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
) )
defaultVideoFormats.add(tmp) defaultVideoFormats.add(tmp)
} }
formatIDPreference.reversed().forEach {
val tmp = Format(
it,
videoContainer!!,
"",
"",
"",
0,
it
)
defaultVideoFormats.add(tmp)
}
bestVideoFormat = defaultVideoFormats.last() bestVideoFormat = defaultVideoFormats.last()
val audioContainer = sharedPreferences.getString("audio_format", "mp3") audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = Format( bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
"best", Format(
audioContainer!!, "best",
"", audioContainer!!,
"", "",
"", "",
0, "",
"best" 0,
) "best"
)
}else{
Format(
audioFormatIDPreference.first(),
audioContainer!!,
"",
"",
"",
0,
audioFormatIDPreference.first()
)
}
videoCodec = sharedPreferences.getString("video_codec", "")
} }
fun deleteDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO) { fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item) repository.delete(id)
} }
suspend fun updateDownload(item: DownloadItem){ suspend fun updateDownload(item: DownloadItem){
if (sharedPreferences.getBoolean("incognito", false)){ if (sharedPreferences.getBoolean("incognito", false)){
if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){ if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){
repository.delete(item) repository.delete(item.id)
} }
}else{ }else{
repository.update(item) repository.update(item)
@ -345,7 +368,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return cloneFormat ( return cloneFormat (
try { try {
try{ try{
formats.first { it.format_note.contains("audio", ignoreCase = true) && audioFormatIDPreference.contains(it.format_id)} formats.filter { it.format_note.contains("audio", ignoreCase = true)}.first {
audioFormatIDPreference.contains(it.format_id) ||
it.container == audioContainer
}
}catch (e: Exception){ }catch (e: Exception){
formats.last { it.format_note.contains("audio", ignoreCase = true) } formats.last { it.format_note.contains("audio", ignoreCase = true) }
} }
@ -358,24 +384,19 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.video -> { Type.video -> {
return cloneFormat( return cloneFormat(
try { try {
val theFormats = formats.ifEmpty { defaultVideoFormats } val theFormats = formats.filter { !it.format_note.contains("audio", true) }.ifEmpty { defaultVideoFormats }
try { when (videoQualityPreference) {
formats.first { !it.format_note.contains("audio", ignoreCase = true) && formatIDPreference.contains(it.format_id)} "worst" -> {
}catch (e: Exception){ theFormats.first()
when (videoQualityPreference) { }
"worst" -> { else /*best*/ -> {
theFormats.first {!it.format_note.contains("audio", ignoreCase = true) } val requirements: MutableList<(Format) -> Boolean> = mutableListOf()
} requirements.add { it: Format -> formatIDPreference.contains(it.format_id) }
"best" -> { requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)}
theFormats.last {!it.format_note.contains("audio", ignoreCase = true) } requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) }
} requirements.add { it: Format -> it.vcodec.startsWith(videoCodec!!, true) }
else -> {
try{ theFormats.reversed().maxByOrNull { f -> requirements.count{ req -> req(f)} } ?: throw Exception()
theFormats.last {it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) }
}catch (e: Exception){
theFormats.last { !it.format_note.contains("audio", ignoreCase = true) }
}
}
} }
} }
}catch (e: Exception){ }catch (e: Exception){
@ -406,6 +427,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val audioFormats = resources.getStringArray(R.array.audio_formats) val audioFormats = resources.getStringArray(R.array.audio_formats)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "") val containerPreference = sharedPreferences.getString("audio_format", "")
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) }
audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) }
return formats return formats
} }
@ -414,6 +436,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val videoFormats = resources.getStringArray(R.array.video_formats_values) val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "") val containerPreference = sharedPreferences.getString("video_format", "")
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) }
videoFormats.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) } videoFormats.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) }
return formats return formats
} }
@ -436,6 +459,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
repository.insert(item) repository.insert(item)
} }
fun pauseDownloads() = viewModelScope.launch(Dispatchers.IO){
repository.pauseDownloads()
}
fun unPauseDownloads() = viewModelScope.launch(Dispatchers.IO){
repository.unPauseDownloads()
}
fun insertAll(items: List<DownloadItem>)= viewModelScope.launch(Dispatchers.IO){ fun insertAll(items: List<DownloadItem>)= viewModelScope.launch(Dispatchers.IO){
items.forEach{ items.forEach{
repository.insert(it) repository.insert(it)
@ -466,6 +497,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getCancelledDownloads() return repository.getCancelledDownloads()
} }
fun getPausedDownloads() : List<DownloadItem> {
return repository.getPausedDownloads()
}
fun getErrored() : List<DownloadItem> { fun getErrored() : List<DownloadItem> {
return repository.getErroredDownloads() return repository.getErroredDownloads()
} }
@ -483,10 +518,24 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getActiveAndQueuedDownloads() return repository.getActiveAndQueuedDownloads()
} }
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items)
startDownloadWorker(emptyList())
}
suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{
dbManager.downloadDao.putAtTopOfTheQueue(id)
startDownloadWorker(emptyList())
}
suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.reQueueDownloadItems(items)
startDownloadWorker(emptyList())
}
suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch { suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
val context = App.instance val context = App.instance
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val queuedItems = mutableListOf<DownloadItem>() val queuedItems = mutableListOf<DownloadItem>()
var exists = false var exists = false
@ -518,33 +567,50 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
} }
if (queuedItems.isNotEmpty()){
startDownloadWorker(queuedItems)
}
}
private suspend fun startDownloadWorker(queuedItems: List<DownloadItem>) {
val context = App.instance
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
queuedItems.forEach { val currentWork = workManager.getWorkInfosByTag("download").await()
if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
var delay = if (it.downloadStartTime != 0L){ var delay = 1000L
it.downloadStartTime - currentTime if (queuedItems.isNotEmpty()){
} else 0 delay = if (queuedItems[0].downloadStartTime != 0L){
if (delay < 0L) delay = 1L queuedItems[0].downloadStartTime.minus(currentTime)
} else 0
if (delay <= 0L) delay = 1000L
}
val workConstraints = Constraints.Builder() val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(Data.Builder().putLong("id", it.id).build())
.addTag(it.id.toString())
.addTag("download") .addTag("download")
.setConstraints(workConstraints.build()) .setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.SECONDS) .setInitialDelay(delay, TimeUnit.MILLISECONDS)
.build()
if (delay > 1000L){
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
}
workManager.enqueueUniqueWork( workManager.enqueueUniqueWork(
it.id.toString(), System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE, ExistingWorkPolicy.REPLACE,
workRequest workRequest.build()
) )
}
}
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){ if (!allowMeteredNetworks && isCurrentNetworkMetered){
@ -553,12 +619,29 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
} }
items.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }?.forEach {
try{ try{
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao) updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
}catch (ignored: Exception){} }catch (ignored: Exception){}
} }
} }
fun getQueuedCollectedFileSize() : Long {
return dbManager.downloadDao.getSelectedFormatFromQueued().sumOf { it.filesize }
}
fun getTotalSize(status: List<DownloadRepository.Status>) : LiveData<Int> {
return dbManager.downloadDao.getDownloadsCountByStatus(status.map { it.toString() }).asLiveData()
}
fun checkAllQueuedItemsAreScheduledAfterNow(items: List<Long>, inverted: Boolean, currentStartTime: Long) : Boolean {
return dbManager.downloadDao.checkAllQueuedItemsAreScheduledAfterNow(items, inverted.toString(), currentStartTime)
}
fun getItemIDsNotPresentIn(items: List<Long>, status: List<DownloadRepository.Status>) : List<Long> {
return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() })
}
private fun updateDownloadItem( private fun updateDownloadItem(
downloadItem: DownloadItem, downloadItem: DownloadItem,
infoUtil: InfoUtil, infoUtil: InfoUtil,

View file

@ -28,10 +28,13 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val items : LiveData<List<ResultItem>> val items : LiveData<List<ResultItem>>
val loadingItems = MutableLiveData<Boolean>() val loadingItems = MutableLiveData<Boolean>()
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
var state: ResultsState = ResultsState.IDLE
enum class ResultsState {
PROCESSING, IDLE
}
init { init {
val dao = DBManager.getInstance(application).resultDao val dao = DBManager.getInstance(application).resultDao
val commandDao = DBManager.getInstance(application).commandTemplateDao
repository = ResultRepository(dao, getApplication<Application>().applicationContext) repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults.asLiveData() items = repository.allResults.asLiveData()
@ -61,6 +64,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
} }
} }
suspend fun parseQueries(inputQueries: List<String>){ suspend fun parseQueries(inputQueries: List<String>){
state = ResultsState.PROCESSING
if (inputQueries.size == 1){ if (inputQueries.size == 1){
parseQuery(inputQueries[0], true) parseQuery(inputQueries[0], true)
}else { }else {
@ -69,6 +73,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
inputQueries.forEach { inputQueries.forEach {
parseQuery(it, false) parseQuery(it, false)
} }
state = ResultsState.IDLE
loadingItems.postValue(false) loadingItems.postValue(false)
} }
} }
@ -97,7 +102,10 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, e.toString()) Log.e(tag, e.toString())
} }
if (resetResults) loadingItems.postValue(false) if (resetResults) {
state = ResultsState.IDLE
loadingItems.postValue(false)
}
res res
} }
} }
@ -107,7 +115,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val m = p.matcher(inputQuery) val m = p.matcher(inputQuery)
if (m.find()) { if (m.find()) {
type = "YT_Video" type = "YT_Video"
if (inputQuery.contains("playlist?list=")) { if (inputQuery.contains("playlist?list=") && !inputQuery.contains("music.youtu")) {
type = "YT_Playlist" type = "YT_Playlist"
} }
} else if (inputQuery.contains("http")) { } else if (inputQuery.contains("http")) {

View file

@ -20,10 +20,8 @@ class CancelDownloadNotificationReceiver : BroadcastReceiver() {
if (message != null) { if (message != null) {
runCatching { runCatching {
val notificationUtil = NotificationUtil(c) val notificationUtil = NotificationUtil(c)
WorkManager.getInstance(c).cancelUniqueWork(id.toString())
notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id)
val dbManager = DBManager.getInstance(c) val dbManager = DBManager.getInstance(c)
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
runCatching { runCatching {

View file

@ -3,8 +3,13 @@ package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PauseDownloadNotificationReceiver : BroadcastReceiver() { class PauseDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) { override fun onReceive(c: Context, intent: Intent) {
@ -16,6 +21,14 @@ class PauseDownloadNotificationReceiver : BroadcastReceiver() {
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
val dbManager = DBManager.getInstance(c)
CoroutineScope(Dispatchers.IO).launch{
runCatching {
val item = dbManager.downloadDao.getDownloadById(id.toLong())
item.status = DownloadRepository.Status.Paused.toString()
dbManager.downloadDao.update(item)
}
}
} }
} }
} }

View file

@ -32,7 +32,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -95,10 +95,9 @@ class ShareActivity : BaseActivity() {
askPermissions() askPermissions()
val action = intent.action val action = intent.action
val type = intent.clipData
Log.e("aa", intent.toString()) Log.e("aa", intent.toString())
if (Intent.ACTION_SEND == action && type != null) { if (Intent.ACTION_SEND == action || Intent.ACTION_VIEW == action) {
if (intent.getStringExtra(Intent.EXTRA_TEXT) == null){ if (intent.getStringExtra(Intent.EXTRA_TEXT) == null && Intent.ACTION_SEND == action){
intent.setClass(this, MainActivity::class.java) intent.setClass(this, MainActivity::class.java)
startActivity(intent) startActivity(intent)
finishAffinity() finishAffinity()
@ -107,7 +106,7 @@ class ShareActivity : BaseActivity() {
runCatching { supportFragmentManager.popBackStack() } runCatching { supportFragmentManager.popBackStack() }
quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false)) quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command")
val loadingBottomSheet = BottomSheetDialog(this) val loadingBottomSheet = BottomSheetDialog(this)
loadingBottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) loadingBottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -118,7 +117,11 @@ class ShareActivity : BaseActivity() {
this.finish() this.finish()
} }
val inputQuery = intent.getStringExtra(Intent.EXTRA_TEXT)!! val inputQuery = when(action){
Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!!
else -> intent.dataString!!
}
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val result = resultViewModel.getItemByURL(inputQuery) val result = resultViewModel.getItemByURL(inputQuery)
@ -166,7 +169,7 @@ class ShareActivity : BaseActivity() {
private fun showSelectPlaylistItems(it: List<ResultItem?>){ private fun showSelectPlaylistItems(it: List<ResultItem?>){
if (sharedPreferences.getBoolean("download_card", true)){ if (sharedPreferences.getBoolean("download_card", true)){
val bottomSheet = SelectPlaylistItemsBottomSheetDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) val bottomSheet = SelectPlaylistItemsDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!))
bottomSheet.show(supportFragmentManager, "downloadPlaylistSheet") bottomSheet.show(supportFragmentManager, "downloadPlaylistSheet")
}else{ }else{
lifecycleScope.launch(Dispatchers.IO){ lifecycleScope.launch(Dispatchers.IO){

View file

@ -6,6 +6,8 @@ import android.content.*
import android.content.Context.CLIPBOARD_SERVICE import android.content.Context.CLIPBOARD_SERVICE
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -40,6 +42,8 @@ import com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.devbrackets.android.exomedia.core.video.mp.NativeVideoDelegate
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
@ -55,6 +59,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.util.* import java.util.*
@ -146,6 +151,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
recyclerView = view.findViewById(R.id.recyclerViewHome) recyclerView = view.findViewById(R.id.recyclerViewHome)
recyclerView?.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) recyclerView?.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
recyclerView?.adapter = homeAdapter recyclerView?.adapter = homeAdapter
recyclerView?.enableFastScroll()
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
resultViewModel.items.observe(viewLifecycleOwner) { resultViewModel.items.observe(viewLifecycleOwner) {
@ -251,12 +257,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if(arguments?.getString("url") == null){ if(arguments?.getString("url") == null){
resultViewModel.checkTrending() if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){
resultViewModel.checkTrending()
}else{
resultViewModel.loadingItems.postValue(true)
}
}else{ }else{
arguments
arguments?.remove("url") arguments?.remove("url")
arguments?.remove("showDownloadsWithUpdatedFormats")
} }
arguments?.remove("showDownloadsWithUpdatedFormats")
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){ if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
lifecycleScope.launch { lifecycleScope.launch {
updateSearchViewItems(searchView?.editText?.text, linkYouCopied) updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
@ -533,7 +542,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
resultViewModel.deleteAll() resultViewModel.deleteAll()
lifecycleScope.launch(Dispatchers.IO){ lifecycleScope.launch(Dispatchers.IO){
Thread.sleep(300) Thread.sleep(300)
if(sharedPreferences!!.getBoolean("quick_download", false)){ if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){
if (queryList.size == 1 && queryList.first().matches("(https?://(?:www\\.|(?!www))[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|www\\.[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|https?://(?:www\\.|(?!www))[a-zA-Z\\d]+\\.\\S{2,}|www\\.[a-zA-Z\\d]+\\.\\S{2,})".toRegex())){ if (queryList.size == 1 && queryList.first().matches("(https?://(?:www\\.|(?!www))[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|www\\.[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|https?://(?:www\\.|(?!www))[a-zA-Z\\d]+\\.\\S{2,}|www\\.[a-zA-Z\\d]+\\.\\S{2,})".toRegex())){
val resultItem = downloadViewModel.createEmptyResultItem(queryList.first()) val resultItem = downloadViewModel.createEmptyResultItem(queryList.first())
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {

View file

@ -37,7 +37,7 @@ import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
class AddExtraCommandsDialog(private val item: DownloadItem, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() { class AddExtraCommandsDialog(private val item: DownloadItem?, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() {
private lateinit var infoUtil: InfoUtil private lateinit var infoUtil: InfoUtil
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
@ -86,9 +86,14 @@ class AddExtraCommandsDialog(private val item: DownloadItem, private val callbac
val text = view.findViewById<EditText>(R.id.command) val text = view.findViewById<EditText>(R.id.command)
val templates = view.findViewById<Button>(R.id.commands) val templates = view.findViewById<Button>(R.id.commands)
val shortcuts = view.findViewById<Button>(R.id.shortcuts) val shortcuts = view.findViewById<Button>(R.id.shortcuts)
val currentCommand = infoUtil.parseYTDLRequestString(infoUtil.buildYoutubeDLRequest(item))
val currentText = view.findViewById<TextView>(R.id.currentText) val currentText = view.findViewById<TextView>(R.id.currentText)
currentText?.text = currentCommand
if (item != null){
val currentCommand = infoUtil.parseYTDLRequestString(infoUtil.buildYoutubeDLRequest(item))
currentText?.text = currentCommand
}else{
view.findViewById<View>(R.id.current).visibility = View.GONE
}
//init syntax highlighter //init syntax highlighter
@ -111,7 +116,10 @@ class AddExtraCommandsDialog(private val item: DownloadItem, private val callbac
highlight.setSpan(currentText) highlight.setSpan(currentText)
currentText.addTextChangedListener(highlightWatcher) currentText.addTextChangedListener(highlightWatcher)
text?.setText(item.extraCommands) highlight.setSpan(text)
text.addTextChangedListener(highlightWatcher)
text?.setText(item?.extraCommands)
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
text!!.postDelayed({ text!!.postDelayed({
text.setSelection(text.length()) text.setSelection(text.length())

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
@ -15,6 +16,7 @@ import android.view.View
import android.widget.* import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem.fromUri import androidx.media3.common.MediaItem.fromUri
import androidx.media3.common.Player import androidx.media3.common.Player
@ -23,6 +25,7 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -37,6 +40,7 @@ import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.RangeSlider
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
@ -68,6 +72,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
private lateinit var toTextInput : TextInputLayout private lateinit var toTextInput : TextInputLayout
private lateinit var cancelBtn : Button private lateinit var cancelBtn : Button
private lateinit var okBtn : Button private lateinit var okBtn : Button
private lateinit var forceKeyframes: MaterialSwitch
private lateinit var suggestedChips: ChipGroup private lateinit var suggestedChips: ChipGroup
private lateinit var suggestedChapters : LinearLayout private lateinit var suggestedChapters : LinearLayout
@ -129,6 +134,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
newCutBtn = view.findViewById(R.id.new_cut) newCutBtn = view.findViewById(R.id.new_cut)
resetBtn = view.findViewById(R.id.reset_all) resetBtn = view.findViewById(R.id.reset_all)
chipGroup = view.findViewById(R.id.cut_list_chip_group) chipGroup = view.findViewById(R.id.cut_list_chip_group)
forceKeyframes = view.findViewById(R.id.force_keyframes)
selectedCuts = if (chipGroup.childCount == 0){ selectedCuts = if (chipGroup.childCount == 0){
@ -152,7 +158,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
if (urls.isNullOrEmpty()) { if (urls.isNullOrEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url) infoUtil.getStreamingUrlAndChapters(item.url)
}else { }else {
urls!!.split("\n").toMutableList() urls.split("\n").toMutableList()
} }
} }
@ -246,6 +252,15 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
} }
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = prefs.edit()
forceKeyframes.isChecked = prefs.getBoolean("force_keyframes", false)
forceKeyframes.setOnCheckedChangeListener { compoundButton, b ->
editor.putBoolean("force_keyframes", forceKeyframes.isChecked)
editor.apply()
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -253,31 +268,25 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
fromTextInput.editText!!.setTextAndRecalculateWidth("0:00") fromTextInput.editText!!.setTextAndRecalculateWidth("0:00")
toTextInput.editText!!.setTextAndRecalculateWidth(item.duration) toTextInput.editText!!.setTextAndRecalculateWidth(item.duration)
rangeSlider.addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener { rangeSlider.setOnTouchListener { v, event -> // Handle touch events here
override fun onStartTrackingTouch(slider: RangeSlider) { when (event.action) {
MotionEvent.ACTION_MOVE -> {
updateFromSlider()
}
MotionEvent.ACTION_UP -> {
updateFromSlider()
}
} }
// Return 'false' to allow the event to continue propagating or 'true' to consume it
false
}
rangeSlider.performClick()
override fun onStopTrackingTouch(slider: RangeSlider) { rangeSlider.setOnDragListener { view, dragEvent ->
val values = rangeSlider.values updateFromSlider()
val startTimestamp = (values[0].toInt() * timeSeconds) / 100 false
val endTimestamp = (values[1].toInt() * timeSeconds) / 100 }
val startTimestampString = infoUtil.formatIntegerDuration(startTimestamp, Locale.US)
val endTimestampString = infoUtil.formatIntegerDuration(endTimestamp, Locale.US)
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString)
okBtn.isEnabled = values[0] != 0F && values[1] != 100F
try {
player.seekTo((startTimestamp * 1000).toLong())
player.play()
}catch (ignored: Exception) {}
}
})
fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener { fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener {
@ -304,8 +313,10 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
startValue = 0F startValue = 0F
} }
fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString())
rangeSlider.setValues(startValue, endValue) rangeSlider.setValues(startValue, endValue)
okBtn.isEnabled = startValue != 0F && endValue != 100F okBtn.isEnabled = startValue != 0F || endValue != 100F
try { try {
player.seekTo(seconds.toLong() * 1000) player.seekTo(seconds.toLong() * 1000)
player.play() player.play()
@ -341,12 +352,14 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
if (seconds == 0) { if (seconds == 0) {
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US)) toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US))
}else if (endValue > 100 || endValue <= rangeSlider.valueFrom.toInt()){ }else if (endValue <= rangeSlider.valueFrom.toInt()){
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US)) toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US))
} }
toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString())
rangeSlider.setValues(startValue, endValue) rangeSlider.setValues(startValue, endValue)
okBtn.isEnabled = startValue != 0F && endValue != 100F okBtn.isEnabled = startValue != 0F || endValue != 100F
try { try {
player.seekTo(startSeconds.toLong() * 1000) player.seekTo(startSeconds.toLong() * 1000)
player.play() player.play()
@ -370,6 +383,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
okBtn.isEnabled = false okBtn.isEnabled = false
okBtn.setOnClickListener { okBtn.setOnClickListener {
forceKeyframes.isVisible = true
val chip = createChip("${fromTextInput.editText!!.text}-${toTextInput.editText!!.text}") val chip = createChip("${fromTextInput.editText!!.text}-${toTextInput.editText!!.text}")
chip.performClick() chip.performClick()
cutSection.visibility = View.GONE cutSection.visibility = View.GONE
@ -379,6 +393,26 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
populateSuggestedChapters() populateSuggestedChapters()
} }
private fun updateFromSlider(){
val values = rangeSlider.values
val startTimestamp = (values[0].toInt() * timeSeconds) / 100
val endTimestamp = (values[1].toInt() * timeSeconds) / 100
val startTimestampString = infoUtil.formatIntegerDuration(startTimestamp, Locale.US)
val endTimestampString = infoUtil.formatIntegerDuration(endTimestamp, Locale.US)
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString)
okBtn.isEnabled = values[0] != 0F || values[1] != 100F
try {
player.seekTo((startTimestamp * 1000).toLong())
player.play()
}catch (ignored: Exception) {}
}
private fun populateSuggestedChapters(){ private fun populateSuggestedChapters(){
if (chapters!!.isEmpty()) suggestedChapters.visibility = View.GONE if (chapters!!.isEmpty()) suggestedChapters.visibility = View.GONE
else { else {
@ -391,6 +425,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
chip.isCheckedIconVisible = false chip.isCheckedIconVisible = false
suggestedChips.addView(chip) suggestedChips.addView(chip)
chip.setOnClickListener { c -> chip.setOnClickListener { c ->
forceKeyframes.isVisible = true
val createdChip = createChapterChip(it, null) val createdChip = createChapterChip(it, null)
createdChip.performClick() createdChip.performClick()
cutSection.visibility = View.GONE cutSection.visibility = View.GONE
@ -424,6 +459,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
} }
if (item.downloadSections.isNotBlank()){ if (item.downloadSections.isNotBlank()){
forceKeyframes.isVisible = true
chipGroup.removeAllViews() chipGroup.removeAllViews()
item.downloadSections.split(";").forEachIndexed { _, it -> item.downloadSections.split(";").forEachIndexed { _, it ->
if (it.isBlank()) return if (it.isBlank()) return

View file

@ -212,143 +212,34 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
if (containers[index] == getString(R.string.defaultValue)) downloadItem.container = "" if (containers[index] == getString(R.string.defaultValue)) downloadItem.container = ""
} }
val embedThumb = view.findViewById<Chip>(R.id.embed_thumb) UiUtil.configureAudio(
embedThumb!!.isChecked = downloadItem.audioPreferences.embedThumb view,
embedThumb.setOnClickListener { requireActivity(),
downloadItem.audioPreferences.embedThumb = embedThumb.isChecked listOf(downloadItem),
} embedThumbClicked = {
downloadItem.audioPreferences.embedThumb = it
val splitByChapters = view.findViewById<Chip>(R.id.split_by_chapters) },
if (downloadItem.downloadSections.isNotBlank()){ splitByChaptersClicked = {
splitByChapters.isEnabled = false downloadItem.audioPreferences.splitByChapters = it
splitByChapters.isChecked = false },
}else{ filenameTemplateSet = {
splitByChapters!!.isChecked = downloadItem.audioPreferences.splitByChapters downloadItem.customFileNameTemplate = it
} },
sponsorBlockItemsSet = { values, checkedItems ->
splitByChapters.setOnClickListener {
downloadItem.audioPreferences.splitByChapters = splitByChapters.isChecked
}
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.file_name_template))
val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = getString(R.string.file_name_template)
editText.setText(downloadItem.customFileNameTemplate)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
getString(R.string.ok)
) { dialog: DialogInterface?, which: Int ->
downloadItem.customFileNameTemplate = editText.text.toString()
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> }
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!!.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(requireContext())
builder.setTitle(getString(R.string.select_sponsorblock_filtering))
val values = resources.getStringArray(R.array.sponsorblock_settings_values)
val entries = resources.getStringArray(R.array.sponsorblock_settings_entries)
val checkedItems : ArrayList<Boolean> = arrayListOf()
values.forEach {
if (downloadItem.audioPreferences.sponsorBlockFilters.contains(it)) {
checkedItems.add(true)
}else{
checkedItems.add(false)
}
}
builder.setMultiChoiceItems(
entries,
checkedItems.toBooleanArray()
) { _, which, isChecked ->
checkedItems[which] = isChecked
}
builder.setPositiveButton(
getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
downloadItem.audioPreferences.sponsorBlockFilters.clear() downloadItem.audioPreferences.sponsorBlockFilters.clear()
for (i in 0 until checkedItems.size) { for (i in 0 until checkedItems.size) {
if (checkedItems[i]) { if (checkedItems[i]) {
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i]) downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
} }
} }
} },
cutClicked = {cutVideoListener ->
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
val cut = view.findViewById<Chip>(R.id.cut)
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 = 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 {
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){ if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener) val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet") bottomSheet.show(parentFragmentManager, "cutVideoSheet")
} }
} },
extraCommandsClicked = {
}else{
cut.isEnabled = false
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands.visibility = View.VISIBLE
extraCommands.setOnClickListener {
val callback = object : ExtraCommandsListener { val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) { override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c downloadItem.extraCommands = c
@ -358,9 +249,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback) val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands") bottomSheetDialog.show(parentFragmentManager, "extraCommands")
} }
}else{ )
extraCommands.visibility = View.GONE
}
}catch (e : Exception){ }catch (e : Exception){
e.printStackTrace() e.printStackTrace()
} }

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
@ -73,8 +74,9 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
if(resources.getBoolean(R.bool.isTablet)){ if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = displayMetrics.heightPixels
} }
} }

View file

@ -151,43 +151,43 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace
)) ))
val newTemplate : Chip = view.findViewById(R.id.newTemplate) UiUtil.configureCommand(
newTemplate.setOnClickListener { view,
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { 1,
templates.add(it) newTemplateClicked = {
chosenCommandView.editText!!.setText(it.content) UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
downloadItem.format = Format( templates.add(it)
it.title, chosenCommandView.editText!!.setText(it.content)
"", downloadItem.format = Format(
"", it.title,
"", "",
"", "",
0, "",
it.content "",
) 0,
populateCommandTemplates(templates, autoCompleteTextView) it.content
)
populateCommandTemplates(templates, autoCompleteTextView)
}
},
editSelectedClicked = {
var current = templates.find { it.title == autoCompleteTextView.text.toString() }
if (current == null) current = CommandTemplate(0, "", chosenCommandView.editText!!.text.toString(), false)
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
}
} }
} )
val editSelected : Chip = view.findViewById(R.id.editSelected)
editSelected.setOnClickListener {
var current = templates.find { it.title == autoCompleteTextView.text.toString() }
if (current == null) current = CommandTemplate(0, "", chosenCommandView.editText!!.text.toString(), false)
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View file

@ -6,6 +6,7 @@ import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
@ -39,6 +40,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
@ -48,6 +50,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -63,6 +66,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.reflect.Type
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -98,8 +102,9 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
if(resources.getBoolean(R.bool.isTablet)){ if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = displayMetrics.heightPixels
} }
} }
@ -112,8 +117,11 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview) recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview)
recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = listAdapter recyclerView.adapter = listAdapter
val itemTouchHelper = ItemTouchHelper(simpleCallback) recyclerView.enableFastScroll()
itemTouchHelper.attachToRecyclerView(recyclerView) if(sharedPreferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
listAdapter.submitList(items.toList()) listAdapter.submitList(items.toList())
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
@ -227,7 +235,9 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
} }
} }
if (items[0].type == DownloadViewModel.Type.command){
bottomAppBar.menu[3].icon?.alpha = 30
}
bottomAppBar.setOnMenuItemClickListener { m: MenuItem -> bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
R.id.folder -> { R.id.folder -> {
@ -264,6 +274,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
listAdapter.notifyDataSetChanged() listAdapter.notifyDataSetChanged()
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel() bottomSheet.cancel()
} }
@ -273,6 +284,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
listAdapter.notifyDataSetChanged() listAdapter.notifyDataSetChanged()
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel() bottomSheet.cancel()
} }
@ -285,6 +297,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
listAdapter.notifyDataSetChanged() listAdapter.notifyDataSetChanged()
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 30
bottomSheet.cancel() bottomSheet.cancel()
} }
} }
@ -299,52 +312,6 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
) )
} }
}
R.id.filename_template -> {
if (items.count { it.type == DownloadViewModel.Type.command } != 0){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint_2), Toast.LENGTH_SHORT).show()
}else{
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.file_name_template))
val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = getString(R.string.file_name_template)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
getString(R.string.ok)
) { dialog: DialogInterface?, which: Int ->
lifecycleScope.launch {
items.forEach {
it.customFileNameTemplate = editText.text.toString()
}
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
}
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> }
val filenameTemplateDialog = builder.create()
editText.doOnTextChanged { text, start, before, count ->
filenameTemplateDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
filenameTemplateDialog.show()
val imm = context?.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
filenameTemplateDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
filenameTemplateDialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
} }
R.id.format -> { R.id.format -> {
if (! items.all { it.type == items[0].type }){ if (! items.all { it.type == items[0].type }){
@ -383,6 +350,136 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
} }
} }
R.id.more -> {
if (! items.all { it.type == items[0].type }) {
Toast.makeText(
requireContext(),
getString(R.string.format_filtering_hint),
Toast.LENGTH_SHORT
).show()
}else{
val scale = resources.displayMetrics.density
val padding = (40*scale*0.5f).toInt()
when(items[0].type){
DownloadViewModel.Type.audio -> {
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.adjust_audio)
val sheetView = bottomSheet.findViewById<View>(android.R.id.content)!!
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
UiUtil.configureAudio(
sheetView,
requireActivity(),
items,
embedThumbClicked = {enabled ->
items.forEach {
it.audioPreferences.embedThumb = enabled
}
},
splitByChaptersClicked = {enabled ->
items.forEach {
it.audioPreferences.splitByChapters = enabled
}
},
filenameTemplateSet = {template ->
items.forEach {
it.customFileNameTemplate = template
}
bottomSheet.dismiss()
},
sponsorBlockItemsSet = { values, checkedItems ->
items.forEach { it.audioPreferences.sponsorBlockFilters.clear() }
for (i in checkedItems.indices) {
if (checkedItems[i]) {
items.forEach { it.audioPreferences.sponsorBlockFilters.add(values[i]) }
}
}
bottomSheet.dismiss()
},
cutClicked = {},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
items.forEach { it.extraCommands = c }
bottomSheet.dismiss()
}
}
val bottomSheetDialog = AddExtraCommandsDialog(null, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
)
bottomSheet.show()
}
DownloadViewModel.Type.video -> {
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.adjust_video)
val sheetView = bottomSheet.findViewById<View>(android.R.id.content)!!
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
UiUtil.configureVideo(
sheetView,
requireActivity(),
items,
embedSubsClicked = {checked ->
items.forEach { it.videoPreferences.embedSubs = checked }
},
addChaptersClicked = {checked ->
items.forEach { it.videoPreferences.addChapters = checked }
},
splitByChaptersClicked = { checked ->
items.forEach { it.videoPreferences.splitByChapters = checked }
},
saveThumbnailClicked = {checked ->
items.forEach { it.SaveThumb = checked }
},
sponsorBlockItemsSet = { values, checkedItems ->
items.forEach { it.videoPreferences.sponsorBlockFilters.clear() }
for (i in checkedItems.indices) {
if (checkedItems[i]) {
items.forEach { it.videoPreferences.sponsorBlockFilters.add(values[i]) }
}
}
bottomSheet.dismiss()
},
cutClicked = {},
filenameTemplateSet = { checked ->
items.forEach { it.customFileNameTemplate = checked }
},
saveSubtitlesClicked = {checked ->
items.forEach { it.videoPreferences.writeSubs = checked }
},
subtitleLanguagesClicked = {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), ""){ value ->
items.forEach { it.videoPreferences.subsLanguages = value }
}
},
removeAudioClicked = {checked ->
items.forEach { it.videoPreferences.removeAudio = checked }
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
items.forEach { it.extraCommands = c }
bottomSheet.dismiss()
}
}
val bottomSheetDialog = AddExtraCommandsDialog(null, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
)
bottomSheet.show()
}
DownloadViewModel.Type.command -> {
}
}
}
}
} }
true true
} }
@ -507,8 +604,12 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
items[i] = item items[i] = item
if (! items.all { it.type == items[0].type }){ if (! items.all { it.type == items[0].type }){
bottomAppBar.menu[1].icon?.alpha = 30 bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}else{ }else{
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
if (items[0].type != DownloadViewModel.Type.command){
bottomAppBar.menu[3].icon?.alpha = 255
}
} }
if (items.count { it.type == DownloadViewModel.Type.command } == 0){ if (items.count { it.type == DownloadViewModel.Type.command } == 0){
@ -581,7 +682,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
super.onChildDraw( super.onChildDraw(
c, c,
recyclerView, recyclerView,
viewHolder!!, viewHolder,
dX, dX,
dY, dY,
actionState, actionState,

View file

@ -221,193 +221,51 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
} }
val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles) UiUtil.configureVideo(
embedSubs!!.isChecked = downloadItem.videoPreferences.embedSubs view,
embedSubs.setOnClickListener { requireActivity(),
downloadItem.videoPreferences.embedSubs = embedSubs.isChecked listOf(downloadItem),
} embedSubsClicked = {
downloadItem.videoPreferences.embedSubs = it
val addChapters = view.findViewById<Chip>(R.id.add_chapters) },
addChapters!!.isChecked = downloadItem.videoPreferences.addChapters addChaptersClicked = {
addChapters.setOnClickListener{ downloadItem.videoPreferences.addChapters = it
downloadItem.videoPreferences.addChapters = addChapters.isChecked },
} splitByChaptersClicked = {
downloadItem.videoPreferences.splitByChapters = it
},
val splitByChapters = view.findViewById<Chip>(R.id.split_by_chapters) saveThumbnailClicked = {
if(downloadItem.downloadSections.isNotBlank()){ downloadItem.SaveThumb = it
splitByChapters.isEnabled = false },
splitByChapters.isChecked = false sponsorBlockItemsSet = { values, checkedItems ->
}else{
splitByChapters!!.isChecked = downloadItem.audioPreferences.splitByChapters
}
splitByChapters.setOnClickListener {
if (splitByChapters.isChecked){
addChapters.isEnabled = false
addChapters.isChecked = false
downloadItem.videoPreferences.addChapters = false
}else{
addChapters.isEnabled = true
}
downloadItem.videoPreferences.splitByChapters = splitByChapters.isChecked
}
val saveThumbnail = view.findViewById<Chip>(R.id.save_thumbnail)
saveThumbnail!!.isChecked = downloadItem.SaveThumb
saveThumbnail.setOnClickListener {
downloadItem.SaveThumb = saveThumbnail.isChecked
}
val sponsorBlock = view.findViewById<Chip>(R.id.sponsorblock_filters)
sponsorBlock!!.setOnClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.select_sponsorblock_filtering))
val values = resources.getStringArray(R.array.sponsorblock_settings_values)
val entries = resources.getStringArray(R.array.sponsorblock_settings_entries)
val checkedItems : ArrayList<Boolean> = arrayListOf()
values.forEach {
if (downloadItem.videoPreferences.sponsorBlockFilters.contains(it)) {
checkedItems.add(true)
}else{
checkedItems.add(false)
}
}
builder.setMultiChoiceItems(
entries,
checkedItems.toBooleanArray()
) { _, which, isChecked ->
checkedItems[which] = isChecked
}
builder.setPositiveButton(
getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
downloadItem.videoPreferences.sponsorBlockFilters.clear() downloadItem.videoPreferences.sponsorBlockFilters.clear()
for (i in 0 until checkedItems.size) { for (i in checkedItems.indices) {
if (checkedItems[i]) { if (checkedItems[i]) {
downloadItem.videoPreferences.sponsorBlockFilters.add(values[i]) downloadItem.videoPreferences.sponsorBlockFilters.add(values[i])
} }
} }
} },
cutClicked = { cutVideoListener ->
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
val cut = view.findViewById<Chip>(R.id.cut)
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 = 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 {
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){ if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener) val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet") bottomSheet.show(parentFragmentManager, "cutVideoSheet")
} }
} },
}else{ filenameTemplateSet = {
cut.isEnabled = false downloadItem.customFileNameTemplate = it
} },
saveSubtitlesClicked = {
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template) downloadItem.videoPreferences.writeSubs = it
filenameTemplate.setOnClickListener { },
val builder = MaterialAlertDialogBuilder(requireContext()) subtitleLanguagesClicked = {
builder.setTitle(getString(R.string.file_name_template)) UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
val inputLayout = layoutInflater.inflate(R.layout.textinput, null) downloadItem.videoPreferences.subsLanguages = it
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext) }
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = getString(R.string.file_name_template) },
editText.setText(downloadItem.customFileNameTemplate) removeAudioClicked = {
editText.setSelection(editText.text.length) downloadItem.videoPreferences.removeAudio = it
builder.setView(inputLayout) },
builder.setPositiveButton( extraCommandsClicked = {
getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
downloadItem.customFileNameTemplate = editText.text.toString()
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
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 saveSubtitles = view.findViewById<Chip>(R.id.save_subtitles)
val subtitleLanguages = view.findViewById<Chip>(R.id.subtitle_languages)
downloadItem.videoPreferences.subsLanguages = sharedPreferences.getString("subs_lang", "en.*,.*-orig")!!
if (downloadItem.videoPreferences.writeSubs) {
saveSubtitles.isChecked = true
subtitleLanguages.visibility = View.VISIBLE
}
saveSubtitles.setOnCheckedChangeListener { _, _ ->
if (saveSubtitles.isChecked) subtitleLanguages.visibility = View.VISIBLE
else subtitleLanguages.visibility = View.GONE
downloadItem.videoPreferences.writeSubs = saveSubtitles.isChecked
}
subtitleLanguages.setOnClickListener {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
downloadItem.videoPreferences.subsLanguages = it
}
}
val removeAudio = view.findViewById<Chip>(R.id.remove_audio)
removeAudio.setOnCheckedChangeListener { _, _ ->
downloadItem.videoPreferences.removeAudio = removeAudio.isChecked
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands.visibility = View.VISIBLE
extraCommands.setOnClickListener {
val callback = object : ExtraCommandsListener { val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) { override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c downloadItem.extraCommands = c
@ -417,9 +275,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback) val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands") bottomSheetDialog.show(parentFragmentManager, "extraCommands")
} }
}else{ )
extraCommands.visibility = View.GONE
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View file

@ -51,7 +51,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var view: View private lateinit var view: View
enum class FormatSorting { enum class FormatSorting {
filesize, container, id filesize, container, codec, id
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -127,7 +127,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
refreshBtn.setOnClickListener { refreshBtn.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
if (items.size > 10){ if (items.size > 10){
continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_INDEFINITE) continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG)
continueInBackgroundSnackBar.setAction(R.string.ok) { continueInBackgroundSnackBar.setAction(R.string.ok) {
listener.onContinueOnBackground(items) listener.onContinueOnBackground(items)
this@FormatSelectionBottomSheetDialog.dismiss() this@FormatSelectionBottomSheetDialog.dismiss()
@ -158,7 +158,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
formats = listOf(res) formats = listOf(res)
//playlist format filtering //list format filtering
}else{ }else{
var progress = "0/${items.size}" var progress = "0/${items.size}"
formatCollection.clear() formatCollection.clear()
@ -168,8 +168,8 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
lifecycleScope.launch(Dispatchers.Main){ lifecycleScope.launch(Dispatchers.Main){
progress = "${formatCollection.size}/${items.size}" progress = "${formatCollection.size}/${items.size}"
refreshBtn.text = progress refreshBtn.text = progress
formatCollection.add(it)
} }
formatCollection.add(it)
} }
} }
formats = formatCollection formats = formatCollection
@ -242,6 +242,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val finalFormats: List<Format> = when(sortBy){ val finalFormats: List<Format> = when(sortBy){
FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value } FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value }
FormatSorting.id -> chosenFormats.sortedBy { it.format_id } FormatSorting.id -> chosenFormats.sortedBy { it.format_id }
FormatSorting.codec -> {
val codecOrder = resources.getStringArray(R.array.video_codec_values).toMutableList()
codecOrder.removeFirst()
chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.startsWith(it) } }
.flatMap {
it.value.sortedBy { l -> l.filesize }
}
}
FormatSorting.filesize -> chosenFormats FormatSorting.filesize -> chosenFormats
} }

View file

@ -6,6 +6,7 @@ import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
@ -28,6 +29,7 @@ import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.paging.filter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
@ -37,7 +39,6 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.adapter.ActiveDownloadMinifiedAdapter import com.deniscerri.ytdlnis.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@ -47,6 +48,7 @@ import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.VideoPlayerUtil import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -58,10 +60,11 @@ import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.FieldPosition
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -73,15 +76,14 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var activeDownloads: ActiveDownloadMinifiedAdapter private lateinit var activeAdapter: ActiveDownloadMinifiedAdapter
private lateinit var queuedDownloads: GenericDownloadAdapter private lateinit var queuedAdapter: GenericDownloadAdapter
private var activeCount: Int = 0 private var activeCount: Int = 0
private lateinit var queuedItems : List<DownloadItem>
private lateinit var activeItems : List<DownloadItem>
private lateinit var downloadManager: DownloadManager private lateinit var downloadManager: DownloadManager
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var dialogView : View
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -89,8 +91,6 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
notificationUtil = NotificationUtil(requireActivity().applicationContext) notificationUtil = NotificationUtil(requireActivity().applicationContext)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
queuedItems = listOf()
activeItems = listOf()
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
} }
@ -101,7 +101,8 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
// Inflate the layout to use as dialog or embedded fragment // Inflate the layout to use as dialog or embedded fragment
return inflater.inflate(R.layout.result_card_details, container, false) dialogView = inflater.inflate(R.layout.result_card_details, container, false)
return dialogView
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -116,14 +117,27 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null) val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.setOnShowListener {
val behavior = BottomSheetBehavior.from(dialogView.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = displayMetrics.heightPixels
}
if (!resources.getBoolean(R.bool.isTablet) && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.maxWidth = displayMetrics.widthPixels
}
}
} }
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activeDownloads = ActiveDownloadMinifiedAdapter(this,requireActivity()) activeAdapter = ActiveDownloadMinifiedAdapter(this,requireActivity())
queuedDownloads = GenericDownloadAdapter(this,requireActivity()) queuedAdapter = GenericDownloadAdapter(this,requireActivity())
val bottomSheetLink = view.findViewById<MaterialButton>(R.id.bottom_sheet_link) val bottomSheetLink = view.findViewById<MaterialButton>(R.id.bottom_sheet_link)
val downloadThumb = view.findViewById<MaterialButton>(R.id.download_thumb) val downloadThumb = view.findViewById<MaterialButton>(R.id.download_thumb)
@ -137,8 +151,8 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
val queuedRecycler = view.findViewById<RecyclerView>(R.id.queued_recycler) val queuedRecycler = view.findViewById<RecyclerView>(R.id.queued_recycler)
val queued = view.findViewById<TextView>(R.id.queued) val queued = view.findViewById<TextView>(R.id.queued)
runningRecycler.adapter = activeDownloads runningRecycler.adapter = activeAdapter
runningRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) runningRecycler.layoutManager = GridLayoutManager(context, 1)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){
@ -146,56 +160,57 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
itemTouchHelper.attachToRecyclerView(queuedRecycler) itemTouchHelper.attachToRecyclerView(queuedRecycler)
} }
queuedRecycler.adapter = queuedDownloads queuedRecycler.adapter = queuedAdapter
queuedRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) queuedRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.activeDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) { downloadViewModel.activeDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) {
if (it.isEmpty()) { activeAdapter.submitList(it)
if (it.isEmpty()){
running.visibility = View.GONE running.visibility = View.GONE
runningRecycler.visibility = View.GONE runningRecycler.visibility = View.GONE
}else{ }else{
running.visibility = View.VISIBLE running.visibility = View.VISIBLE
runningRecycler.visibility = View.VISIBLE runningRecycler.visibility = View.VISIBLE
}
}
it.forEach{item -> WorkManager.getInstance(requireContext())
WorkManager.getInstance(requireContext()) .getWorkInfosByTagLiveData("download")
.getWorkInfosForUniqueWorkLiveData(item.id.toString()) .observe(viewLifecycleOwner){ list ->
.observe(viewLifecycleOwner){ list -> list.forEach {work ->
list.forEach {work -> if (work == null) return@forEach
if (work == null) return@observe val id = work.progress.getLong("id", 0L)
val id = work.progress.getLong("id", 0L) if(id == 0L) return@forEach
if(id == 0L) return@observe
val progress = work.progress.getInt("progress", 0) val progress = work.progress.getInt("progress", 0)
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress") requireActivity().runOnUiThread {
try {
requireActivity().runOnUiThread { progressBar?.setProgressCompat(progress, true)
try { }catch (ignored: Exception) {}
progressBar?.setProgressCompat(progress, true) }
}catch (ignored: Exception) {}
}
}
}
} }
} }
activeItems = it lifecycleScope.launch {
activeDownloads.submitList(it) downloadViewModel.queuedDownloads.map { it.filter { d -> d.url == item.url } }.collectLatest {
queuedAdapter.submitData(it)
}
} }
queuedAdapter.addLoadStateListener { loadState ->
downloadViewModel.queuedDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) { lifecycleScope.launch {
if (it.isEmpty()) { if (loadState.append.endOfPaginationReached )
queued.visibility = View.GONE {
queuedRecycler.visibility = View.GONE if (queuedAdapter.itemCount < 1){
}else{ queued.visibility = View.GONE
queued.visibility = View.VISIBLE queuedRecycler.visibility = View.GONE
queuedRecycler.visibility = View.VISIBLE }else{
queued.visibility = View.VISIBLE
queuedRecycler.visibility = View.VISIBLE
}
}
} }
queuedItems = it
queuedDownloads.submitList(it)
} }
downloadViewModel.activeDownloadsCount.observe(viewLifecycleOwner){ downloadViewModel.activeDownloadsCount.observe(viewLifecycleOwner){
@ -238,23 +253,9 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
videoView = view.findViewById(R.id.video_view) videoView = view.findViewById(R.id.video_view)
val pause = view.findViewById<MaterialButton>(R.id.pause)
val player = VideoPlayerUtil.buildPlayer(requireContext()) val player = VideoPlayerUtil.buildPlayer(requireContext())
videoView.player = player videoView.player = player
videoView.setOnClickListener {
if (player.isPlaying){
player.pause()
pause.visibility = View.VISIBLE
}
else {
player.play()
pause.visibility = View.GONE
}
}
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val data : MutableList<String?> = withContext(Dispatchers.IO){ val data : MutableList<String?> = withContext(Dispatchers.IO){
@ -324,25 +325,29 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
} }
private fun removeQueuedItem(id: Long){ private fun removeQueuedItem(id: Long){
val item = queuedItems.find { it.id == id } ?: return lifecycleScope.launch {
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) val item = withContext(Dispatchers.IO){
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") downloadViewModel.getItemByID(id)
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
} }
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
Snackbar.make(requireView().rootView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG) Snackbar.make(requireView().rootView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) { .setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item) downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.queueDownloads(listOf(item))
} }
}.show() }.show()
}
deleteDialog.show()
} }
deleteDialog.show()
} }
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
@ -353,12 +358,16 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = queuedItems[position] lifecycleScope.launch {
queuedDownloads.notifyItemChanged(position) val deletedItem = withContext(Dispatchers.IO){
removeQueuedItem(deletedItem.id) downloadViewModel.getItemByID(itemID)
}
queuedAdapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeQueuedItem(deletedItem.id)
}
} }
} }
@ -396,7 +405,7 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
super.onChildDraw( super.onChildDraw(
c, c,
recyclerView, recyclerView,
viewHolder!!, viewHolder,
dX, dX,
dY, dY,
actionState, actionState,
@ -410,133 +419,135 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
} }
override fun onCardClick(itemID: Long) { override fun onCardClick(itemID: Long) {
val item = queuedItems.find { it.id == itemID } ?: return lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
val bottomSheet = BottomSheetDialog(requireContext()) downloadViewModel.getItemByID(itemID)
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 { "`${requireContext().getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${requireContext().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(requireContext(), R.drawable.ic_music)
} }
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video) val bottomSheet = BottomSheetDialog(requireContext())
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 { "`${requireContext().getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${requireContext().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(requireContext(), R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
}
} }
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), 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)
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 {
UiUtil.showDatePicker(parentFragmentManager) {
bottomSheet.dismiss()
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(item.id)
item.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
}
}
} }
}
val time = bottomSheet.findViewById<Chip>(R.id.time) if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note) View.GONE
val container = bottomSheet.findViewById<Chip>(R.id.container_chip) else formatNote!!.text = item.format.format_note
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE if (item.format.container != "") container!!.text = item.format.container.uppercase()
else { else container!!.visibility = View.GONE
val calendar = Calendar.getInstance()
calendar.timeInMillis = item.downloadStartTime
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
time.setOnClickListener { val codecText =
UiUtil.showDatePicker(parentFragmentManager) { 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 = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
bottomSheet.hide()
removeQueuedItem(itemID)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
else{
downloadNow!!.text = getString(R.string.download_now)
downloadNow.setOnClickListener {
bottomSheet.dismiss() bottomSheet.dismiss()
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show() downloadViewModel.deleteDownload(item.id)
downloadViewModel.deleteDownload(item) item.downloadStartTime = 0
item.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString()) WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking { runBlocking {
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.queueDownloads(listOf(item))
} }
} }
} }
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
} }
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 = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
bottomSheet.hide()
removeQueuedItem(itemID)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
else{
downloadNow!!.text = getString(R.string.download_now)
downloadNow.setOnClickListener {
bottomSheet.dismiss()
downloadViewModel.deleteDownload(item)
item.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(isChecked: Boolean, position: Int) {}
}
override fun onCancelClick(itemID: Long) { override fun onCancelClick(itemID: Long) {
lifecycleScope.launch { lifecycleScope.launch {
@ -557,41 +568,45 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
cancelActiveDownload(itemID) cancelActiveDownload(itemID)
} }
override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) { override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) {
val item = activeItems.find { it.id == itemID } ?: return lifecycleScope.launch {
when(action){ val item = withContext(Dispatchers.IO){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> { downloadViewModel.getItemByID(itemID)
lifecycleScope.launch {
cancelItem(itemID.toInt())
item.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
}
} }
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> { when(action){
lifecycleScope.launch { ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
item.status = DownloadRepository.Status.Active.toString() lifecycleScope.launch {
withContext(Dispatchers.IO){ cancelItem(itemID.toInt())
downloadViewModel.updateDownload(item) item.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeAdapter.notifyItemChanged(position)
} }
activeDownloads.notifyItemChanged(position) }
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch {
item.status = DownloadRepository.Status.Active.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeAdapter.notifyItemChanged(position)
val queue = if (activeCount > 1) listOf(item) val queue = if (activeCount > 1) listOf(item)
else withContext(Dispatchers.IO){ else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList() val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() } list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item) list.add(0, item)
list list
} }
runBlocking { runBlocking {
downloadViewModel.queueDownloads(queue) downloadViewModel.queueDownloads(queue)
}
} }
} }
} }
} }
} }
override fun onCardClick() { override fun onCardClick() {
this.dismiss() this.dismiss()
@ -607,15 +622,19 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
} }
private fun cancelActiveDownload(itemID: Long){ private fun cancelActiveDownload(itemID: Long){
val id = itemID.toInt() lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(id.toString()) val id = itemID.toInt()
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id) WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id)
activeItems.find { it.id == itemID }?.let { withContext(Dispatchers.IO){
it.status = DownloadRepository.Status.Cancelled.toString() downloadViewModel.getItemByID(itemID)
lifecycleScope.launch(Dispatchers.IO){ }.let {
downloadViewModel.updateDownload(it) it.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
} }
} }

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.ActionBar.LayoutParams
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
@ -8,9 +9,12 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -22,44 +26,56 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.setTextAndRecalculateWidth import com.deniscerri.ytdlnis.util.UiUtil.setTextAndRecalculateWidth
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>, private val type: DownloadViewModel.Type) : BottomSheetDialogFragment(), PlaylistAdapter.OnItemClickListener { class SelectPlaylistItemsDialog(private val items: List<ResultItem?>, private val type: DownloadViewModel.Type) : DialogFragment(), PlaylistAdapter.OnItemClickListener {
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var listAdapter : PlaylistAdapter private lateinit var listAdapter : PlaylistAdapter
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var ok: MenuItem
private lateinit var selectedText: TextView private lateinit var toolbar: MaterialToolbar
private lateinit var fromTextInput: TextInputLayout private lateinit var fromTextInput: TextInputLayout
private lateinit var toTextInput: TextInputLayout private lateinit var toTextInput: TextInputLayout
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.FullScreenDialogTheme)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
} }
@SuppressLint("RestrictedApi") override fun onCreateView(
override fun setupDialog(dialog: Dialog, style: Int) { inflater: LayoutInflater,
super.setupDialog(dialog, style) container: ViewGroup?,
val view = LayoutInflater.from(context).inflate(R.layout.select_playlist_items, null) savedInstanceState: Bundle?
dialog.setContentView(view) ): View? {
return inflater.inflate(R.layout.select_playlist_items, container, false)
}
dialog.setOnShowListener { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
behavior = BottomSheetBehavior.from(view.parent as View) val dialog = super.onCreateDialog(savedInstanceState)
val displayMetrics = DisplayMetrics() dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) return dialog
behavior.state = BottomSheetBehavior.STATE_EXPANDED }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
toolbar = view.findViewById<MaterialToolbar>(R.id.toolbar)
listAdapter = listAdapter =
PlaylistAdapter( PlaylistAdapter(
@ -70,10 +86,10 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview) recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview)
recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = listAdapter recyclerView.adapter = listAdapter
recyclerView.enableFastScroll()
listAdapter.submitList(items) listAdapter.submitList(items)
selectedText = view.findViewById<Button>(R.id.selected) toolbar.title = "0 ${resources.getString(R.string.selected)}"
selectedText.text = "0 ${resources.getString(R.string.selected)}"
fromTextInput = view.findViewById(R.id.from_textinput) fromTextInput = view.findViewById(R.id.from_textinput)
toTextInput = view.findViewById(R.id.to_textinput) toTextInput = view.findViewById(R.id.to_textinput)
@ -93,14 +109,14 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
if (startNr <= 0) startNr = 0 if (startNr <= 0) startNr = 0
if (endNr > items.size) endNr = items.size - 1 if (endNr > items.size) endNr = items.size - 1
listAdapter.checkRange(startNr, endNr) listAdapter.checkRange(startNr, endNr)
selectedText.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}" ok.isEnabled = true
toolbar.title = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
} }
} }
} }
toTextInput.editText!!.doAfterTextChanged { _text -> toTextInput.editText!!.doAfterTextChanged { _text ->
reset() reset()
val start = fromTextInput.editText!!.text.toString() val start = fromTextInput.editText!!.text.toString()
val end = _text.toString() val end = _text.toString()
@ -113,7 +129,8 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
if (startNr <= 0) startNr = 0 if (startNr <= 0) startNr = 0
if (endNr > items.size) endNr = items.size -1 if (endNr > items.size) endNr = items.size -1
listAdapter.checkRange(startNr, endNr) listAdapter.checkRange(startNr, endNr)
selectedText.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}" ok.isEnabled = true
toolbar.title = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
} }
} }
} }
@ -126,19 +143,22 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
listAdapter.checkAll() listAdapter.checkAll()
fromTextInput.isEnabled = true fromTextInput.isEnabled = true
toTextInput.isEnabled = true toTextInput.isEnabled = true
selectedText.text = resources.getString(R.string.all_items_selected) ok.isEnabled = true
toolbar.title = resources.getString(R.string.all_items_selected)
}else{ }else{
reset() reset()
fromTextInput.isEnabled = true fromTextInput.isEnabled = true
toTextInput.isEnabled = true toTextInput.isEnabled = true
ok.isEnabled = false
fromTextInput.editText!!.setTextAndRecalculateWidth("") fromTextInput.editText!!.setTextAndRecalculateWidth("")
toTextInput.editText!!.setTextAndRecalculateWidth("") toTextInput.editText!!.setTextAndRecalculateWidth("")
} }
} }
val ok = view.findViewById<Button>(R.id.bottomsheet_ok) ok = toolbar.menu.getItem(0)
ok!!.setOnClickListener { ok.isEnabled = false
ok.setOnMenuItemClickListener {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) } val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
@ -164,6 +184,7 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
dismiss() dismiss()
} }
true
} }
view.findViewById<BottomAppBar>(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem -> view.findViewById<BottomAppBar>(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem ->
@ -172,17 +193,22 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
listAdapter.invertSelected(items) listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){ if (checkedItems.size == items.size){
selectedText.text = resources.getString(R.string.all_items_selected) toolbar.title= resources.getString(R.string.all_items_selected)
}else{ }else{
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}" toolbar.title = "${checkedItems.size} ${resources.getString(R.string.selected)}"
} }
if(checkedItems.isNotEmpty() && checkedItems.size < items.size){ if(checkedItems.isNotEmpty() && checkedItems.size < items.size){
fromTextInput.isEnabled = false fromTextInput.isEnabled = false
toTextInput.isEnabled = false toTextInput.isEnabled = false
} }
ok.isEnabled = checkedItems.isNotEmpty()
} }
true true
} }
toolbar.setNavigationOnClickListener {
requireActivity().finishAffinity()
}
} }
override fun onCancel(dialog: DialogInterface) { override fun onCancel(dialog: DialogInterface) {
@ -209,21 +235,24 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
} }
private fun reset(){ private fun reset(){
ok.isEnabled = false
listAdapter.clearCheckeditems() listAdapter.clearCheckeditems()
selectedText.text = "0 ${resources.getString(R.string.selected)}" toolbar.title = "0 ${resources.getString(R.string.selected)}"
} }
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) { override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
if (checkedItems.size == items.size){ if (checkedItems.size == items.size){
selectedText.text = resources.getString(R.string.all_items_selected) toolbar.title = resources.getString(R.string.all_items_selected)
}else{ }else{
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}" toolbar.title = "${checkedItems.size} ${resources.getString(R.string.selected)}"
} }
if (checkedItems.isEmpty()){ if (checkedItems.isEmpty()){
ok.isEnabled = false
fromTextInput.isEnabled = true fromTextInput.isEnabled = true
toTextInput.isEnabled = true toTextInput.isEnabled = true
}else{ }else{
ok.isEnabled = true
fromTextInput.isEnabled = false fromTextInput.isEnabled = false
toTextInput.isEnabled = false toTextInput.isEnabled = false
} }

View file

@ -10,9 +10,11 @@ import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
@ -39,7 +41,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private lateinit var activeDownloads : ActiveDownloadAdapter private lateinit var activeDownloads : ActiveDownloadAdapter
lateinit var downloadItem: DownloadItem lateinit var downloadItem: DownloadItem
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var list: List<DownloadItem>
private lateinit var pauseResume: MaterialButton private lateinit var pauseResume: MaterialButton
private lateinit var workManager: WorkManager private lateinit var workManager: WorkManager
@ -53,7 +54,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
activity = getActivity() activity = getActivity()
notificationUtil = NotificationUtil(requireContext()) notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
list = listOf()
workManager = WorkManager.getInstance(requireContext()) workManager = WorkManager.getInstance(requireContext())
return fragmentView return fragmentView
} }
@ -77,6 +77,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
if (pauseResume.text == requireContext().getString(R.string.pause)){ if (pauseResume.text == requireContext().getString(R.string.pause)){
lifecycleScope.launch { lifecycleScope.launch {
pauseResume.isEnabled = false
val queued = withContext(Dispatchers.IO){ val queued = withContext(Dispatchers.IO){
downloadViewModel.getQueued() downloadViewModel.getQueued()
} }
@ -92,18 +93,28 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager.cancelAllWorkByTag(it.id.toString()) workManager.cancelAllWorkByTag(it.id.toString())
} }
list.forEach { val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
active.forEach {
cancelItem(it.id.toInt()) cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString() it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
} }
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true
} }
}else{ }else{
lifecycleScope.launch { lifecycleScope.launch {
val toQueue = list.filter { it.status == DownloadRepository.Status.Paused.toString() }.toMutableList() pauseResume.isEnabled = false
val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
val toQueue = active.filter { it.status == DownloadRepository.Status.Paused.toString() }.toMutableList()
toQueue.forEach { toQueue.forEach {
it.status = DownloadRepository.Status.Active.toString() it.status = DownloadRepository.Status.Active.toString()
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
@ -116,17 +127,40 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
runBlocking { runBlocking {
downloadViewModel.queueDownloads(toQueue) downloadViewModel.queueDownloads(toQueue)
} }
pauseResume.isEnabled = true
} }
} }
} }
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("download")
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (work == null) return@forEach
val id = work.progress.getLong("id", 0L)
if(id == 0L) return@forEach
val progress = work.progress.getInt("progress", 0)
val output = work.progress.getString("output")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val outputText = view.findViewWithTag<TextView>("$id##output")
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(progress, true)
outputText?.text = output
}catch (ignored: Exception) {}
}
}
}
downloadViewModel.activeDownloads.observe(viewLifecycleOwner) { downloadViewModel.activeDownloads.observe(viewLifecycleOwner) {
list = it
activeDownloads.submitList(it) activeDownloads.submitList(it)
if (it.size > 1){ if (it.size > 1){
pauseResume.visibility = View.VISIBLE pauseResume.visibility = View.VISIBLE
if (list.all { l -> l.status == DownloadRepository.Status.Paused.toString() }){ if (it.all { l -> l.status == DownloadRepository.Status.Paused.toString() }){
pauseResume.text = requireContext().getString(R.string.resume) pauseResume.text = requireContext().getString(R.string.resume)
}else{ }else{
pauseResume.text = requireContext().getString(R.string.pause) pauseResume.text = requireContext().getString(R.string.pause)
@ -135,37 +169,16 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pauseResume.visibility = View.GONE pauseResume.visibility = View.GONE
} }
it.forEach{item ->
WorkManager.getInstance(requireContext())
.getWorkInfosForUniqueWorkLiveData(item.id.toString())
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (work == null) return@observe
val id = work.progress.getLong("id", 0L)
if(id == 0L) return@observe
val progress = work.progress.getInt("progress", 0)
val output = work.progress.getString("output")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val outputText = view.findViewWithTag<TextView>("$id##output")
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(progress, true)
outputText?.text = output
}catch (ignored: Exception) {}
}
}
}
}
} }
} }
override fun onCancelClick(itemID: Long) { override fun onCancelClick(itemID: Long) {
lifecycleScope.launch { lifecycleScope.launch {
if (list.size == 1){ val count = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.count()
if (count == 1){
val queue = withContext(Dispatchers.IO){ val queue = withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList() val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() } list.map { it.status = DownloadRepository.Status.Queued.toString() }
@ -183,41 +196,50 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) { override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) {
val item = list.find { it.id == itemID } ?: return lifecycleScope.launch {
when(action){ val item = withContext(Dispatchers.IO){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> { downloadViewModel.getItemByID(itemID)
lifecycleScope.launch {
cancelItem(itemID.toInt())
item.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
}
} }
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch {
item.status = DownloadRepository.Status.Active.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
val queue = if (list.size > 1) listOf(item) when(action){
else withContext(Dispatchers.IO){ ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
val list = downloadViewModel.getQueued().toMutableList() lifecycleScope.launch {
list.map { it.status = DownloadRepository.Status.Queued.toString() } cancelItem(itemID.toInt())
list.add(0, item) item.status = DownloadRepository.Status.Paused.toString()
list withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
} }
}
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch {
item.status = DownloadRepository.Status.Active.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
runBlocking { val count = withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(queue) downloadViewModel.getActiveDownloads()
}.count()
val queue = if (count > 1) listOf(item)
else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item)
list
}
runBlocking {
downloadViewModel.queueDownloads(queue)
}
} }
} }
} }
}
}
} }
override fun onOutputClick(item: DownloadItem) { override fun onOutputClick(item: DownloadItem) {
@ -238,17 +260,19 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
private fun cancelDownload(itemID: Long){ private fun cancelDownload(itemID: Long){
cancelItem(itemID.toInt()) lifecycleScope.launch {
list.find { it.id == itemID }?.let { cancelItem(itemID.toInt())
it.status = DownloadRepository.Status.Cancelled.toString() withContext(Dispatchers.IO){
lifecycleScope.launch(Dispatchers.IO){ downloadViewModel.getItemByID(itemID)
downloadViewModel.updateDownload(it) }.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
} }
} }
} }
override fun onClick(p0: View?) { override fun onClick(p0: View?) {
TODO("Not yet implemented")
} }
} }

View file

@ -1,28 +1,21 @@
package com.deniscerri.ytdlnis.ui.downloads package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
@ -30,20 +23,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener { class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -51,10 +46,9 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var cancelledRecyclerView : RecyclerView private lateinit var cancelledRecyclerView : RecyclerView
private lateinit var cancelledDownloads : GenericDownloadAdapter private lateinit var adapter : GenericDownloadAdapter
private var selectedObjects: ArrayList<DownloadItem>? = null
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem> private var totalSize = 0
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -63,16 +57,14 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
): View? { ): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false) fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
activity = getActivity() activity = getActivity()
selectedObjects = arrayListOf()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
return fragmentView return fragmentView
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cancelledDownloads = adapter =
GenericDownloadAdapter( GenericDownloadAdapter(
this, this,
requireActivity() requireActivity()
@ -80,7 +72,8 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
cancelledRecyclerView = view.findViewById(R.id.download_recyclerview) cancelledRecyclerView = view.findViewById(R.id.download_recyclerview)
cancelledRecyclerView.forceFastScrollMode() cancelledRecyclerView.forceFastScrollMode()
cancelledRecyclerView.adapter = cancelledDownloads cancelledRecyclerView.adapter = adapter
cancelledRecyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback) val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -89,146 +82,74 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
cancelledRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) cancelledRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.cancelledDownloads.observe(viewLifecycleOwner) { lifecycleScope.launch {
items = it.toMutableList() downloadViewModel.cancelledDownloads.collectLatest {
cancelledDownloads.submitList(it) adapter.submitData(it)
}
}
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Cancelled)).observe(viewLifecycleOwner){
totalSize = it
} }
} }
override fun onActionButtonClick(itemID: Long) { override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID } lifecycleScope.launch {
if (item != null){ runCatching {
runBlocking { val item = withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(listOf(item))
}
}.onFailure {
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
} }
}else{
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
} }
} }
override fun onCardClick(itemID: Long) { override fun onCardClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
val bottomSheet = BottomSheetDialog(requireContext()) downloadViewModel.getItemByID(itemID)
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 { "`${requireContext().getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${requireContext().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(requireContext(), R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
} }
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
removeItem(it, sheet)
},
downloadItem = {
runBlocking{
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {}
)
} }
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)
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 = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
removeItem(item, bottomSheet)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val redownload = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
redownload!!.tag = itemID
redownload.setOnClickListener{
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
bottomSheet.cancel()
}
redownload.setOnLongClickListener {
bottomSheet.cancel()
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(item), item.type, item, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
true
}
openFile.visibility = View.GONE
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(isChecked: Boolean, position: Int) {
val item = items.find { it.id == itemID } lifecycleScope.launch {
if (isChecked) { val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
selectedObjects!!.add(item!!) if (isChecked) {
if (actionMode == null){ if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
}else{ actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" }
} }
} else {
else { actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
selectedObjects!!.remove(item) if (selectedObjects == 0){
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode?.finish()
if (selectedObjects!!.isEmpty()){ }
actionMode?.finish()
} }
} }
} }
@ -240,7 +161,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
downloadViewModel.deleteDownload(item) downloadViewModel.deleteDownload(item.id)
} }
deleteDialog.show() deleteDialog.show()
} }
@ -249,7 +170,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
private val contextualActionBar = object : ActionMode.Callback { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.cancelled_downloads_menu_context, menu) mode!!.menuInflater.inflate(R.menu.cancelled_downloads_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}"
return true return true
} }
@ -270,39 +191,56 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){ lifecycleScope.launch {
downloadViewModel.deleteDownload(obj) val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Cancelled))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
for (id in selectedObjects){
downloadViewModel.deleteDownload(id)
}
actionMode?.finish()
} }
clearCheckedItems()
actionMode?.finish()
} }
deleteDialog.show() deleteDialog.show()
true true
} }
R.id.redownload -> { R.id.redownload -> {
lifecycleScope.launch {
val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Cancelled))
}
}else{
adapter.checkedItems.toList()
}
withContext(Dispatchers.IO){
downloadViewModel.reQueueDownloadItems(selectedObjects)
}
adapter.clearCheckedItems()
}
runBlocking { runBlocking {
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList())
actionMode?.finish() actionMode?.finish()
} }
true true
} }
R.id.select_all -> { R.id.select_all -> {
cancelledDownloads.checkAll(items) adapter.checkAll()
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {
cancelledDownloads.invertSelected(items) adapter.invertSelected()
val invertedList = arrayListOf<DownloadItem>() val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
items.forEach { actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (!selectedObjects?.contains(it)!!) invertedList.add(it) if (selectedObjects == 0) actionMode?.finish()
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
else -> false else -> false
@ -311,15 +249,10 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null actionMode = null
clearCheckedItems() adapter.clearCheckedItems()
} }
} }
private fun clearCheckedItems(){
cancelledDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
@ -328,18 +261,22 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = items[position] lifecycleScope.launch {
downloadViewModel.deleteDownload(deletedItem) val deletedItem = withContext(Dispatchers.IO){
Snackbar.make(cancelledRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) downloadViewModel.getItemByID(itemID)
.setAction(getString(R.string.undo)) { }
downloadViewModel.insert(deletedItem) downloadViewModel.deleteDownload(deletedItem.id)
}.show() Snackbar.make(cancelledRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
downloadViewModel.insert(deletedItem)
}.show()
}
} }
ItemTouchHelper.RIGHT -> { ItemTouchHelper.RIGHT -> {
onActionButtonClick(items[position].id) onActionButtonClick(itemID)
} }
} }
} }

View file

@ -105,7 +105,10 @@ class DownloadQueueMainFragment : Fragment(){
initMenu() initMenu()
if (arguments?.getString("tab") != null){ if (arguments?.getString("tab") != null){
tabLayout.selectTab(tabLayout.getTabAt(3)) tabLayout.getTabAt(3)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(3, false)
}, 200)
} }
} }

View file

@ -1,28 +1,22 @@
package com.deniscerri.ytdlnis.ui.downloads package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemClickListener
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@ -31,19 +25,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener, OnItemClickListener { class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener, OnItemClickListener {
@ -51,10 +48,9 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var erroredRecyclerView : RecyclerView private lateinit var erroredRecyclerView : RecyclerView
private lateinit var erroredDownloads : GenericDownloadAdapter private lateinit var adapter : GenericDownloadAdapter
private lateinit var items : MutableList<DownloadItem>
private var selectedObjects: ArrayList<DownloadItem>? = null
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private var totalSize: Int = 0
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -63,15 +59,13 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false) fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
activity = getActivity() activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
selectedObjects = arrayListOf()
return fragmentView return fragmentView
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
erroredDownloads = adapter =
GenericDownloadAdapter( GenericDownloadAdapter(
this, this,
requireActivity() requireActivity()
@ -79,7 +73,8 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
erroredRecyclerView = view.findViewById(R.id.download_recyclerview) erroredRecyclerView = view.findViewById(R.id.download_recyclerview)
erroredRecyclerView.forceFastScrollMode() erroredRecyclerView.forceFastScrollMode()
erroredRecyclerView.adapter = erroredDownloads erroredRecyclerView.adapter = adapter
erroredRecyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback) val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -87,147 +82,77 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
} }
erroredRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) erroredRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.erroredDownloads.observe(viewLifecycleOwner) { lifecycleScope.launch {
items = it.toMutableList() downloadViewModel.erroredDownloads.collectLatest {
erroredDownloads.submitList(it) adapter.submitData(it)
}
}
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Error)).observe(viewLifecycleOwner){
totalSize = it
} }
} }
override fun onActionButtonClick(itemID: Long) { override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return lifecycleScope.launch {
if (item.logID != null) { val item = withContext(Dispatchers.IO){
val bundle = Bundle() downloadViewModel.getItemByID(itemID)
bundle.putLong("logID", item.logID!!) }
findNavController().navigate( if (item.logID != null) {
R.id.downloadLogFragment, val bundle = Bundle()
bundle bundle.putLong("logID", item.logID!!)
) findNavController().navigate(
R.id.downloadLogFragment,
bundle
)
}
} }
} }
override fun onCardClick(itemID: Long) { override fun onCardClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
val bottomSheet = BottomSheetDialog(requireContext()) UiUtil.showDownloadItemDetailsCard(
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) item,
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet) requireActivity(),
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title) DownloadRepository.Status.valueOf(item.status),
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" } removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author) removeItem(it, sheet)
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
// BUTTON ---------------------------------- },
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type) downloadItem = {
runBlocking{
when (item.type) { downloadViewModel.queueDownloads(listOf(it))
DownloadViewModel.Type.audio -> { }
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music) },
} longClickDownloadButton = {
DownloadViewModel.Type.video -> { val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video) sheet.show(parentFragmentManager, "downloadSingleSheet")
} },
else -> { scheduleButtonClick = {}
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal) )
} }
}
val time = bottomSheet.findViewById<Chip>(R.id.time)
val formatNote = bottomSheet.findViewById<TextView>(R.id.format_note)
val container = bottomSheet.findViewById<TextView>(R.id.container_chip)
val codec = bottomSheet.findViewById<TextView>(R.id.codec)
val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size)
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 = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
removeItem(item, bottomSheet)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val redownload = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
redownload!!.tag = itemID
redownload.setOnClickListener{
runBlocking{
downloadViewModel.queueDownloads(listOf(item))
}
bottomSheet.cancel()
}
redownload.setOnLongClickListener {
bottomSheet.cancel()
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(item), item.type, item, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
true
}
openFile.visibility = View.GONE
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(isChecked: Boolean, position: Int) {
val item = items.find { it.id == itemID } lifecycleScope.launch {
if (isChecked) { val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
selectedObjects!!.add(item!!) if (isChecked) {
if (actionMode == null){ if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{ }else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
}
} }
} else {
else { actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
selectedObjects!!.remove(item) if (selectedObjects == 0){
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode?.finish()
if (selectedObjects!!.isEmpty()){ }
actionMode?.finish()
} }
} }
} }
@ -238,7 +163,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
downloadViewModel.deleteDownload(item) downloadViewModel.deleteDownload(item.id)
} }
deleteDialog.show() deleteDialog.show()
} }
@ -252,7 +177,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
private val contextualActionBar = object : ActionMode.Callback { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.cancelled_downloads_menu_context, menu) mode!!.menuInflater.inflate(R.menu.cancelled_downloads_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}"
return true return true
} }
@ -273,39 +198,53 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){ lifecycleScope.launch {
downloadViewModel.deleteDownload(obj) val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Error))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
for (id in selectedObjects){
downloadViewModel.deleteDownload(id)
}
actionMode?.finish()
} }
clearCheckedItems()
actionMode?.finish()
} }
deleteDialog.show() deleteDialog.show()
true true
} }
R.id.redownload -> { R.id.redownload -> {
runBlocking { lifecycleScope.launch {
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList()) val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Error))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.reQueueDownloadItems(selectedObjects)
}
actionMode?.finish() actionMode?.finish()
} }
true true
} }
R.id.select_all -> { R.id.select_all -> {
erroredDownloads.checkAll(items) adapter.checkAll()
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {
erroredDownloads.invertSelected(items) adapter.invertSelected()
val invertedList = arrayListOf<DownloadItem>() val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
items.forEach { actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (!selectedObjects?.contains(it)!!) invertedList.add(it) if (selectedObjects == 0) actionMode?.finish()
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
else -> false else -> false
@ -314,15 +253,10 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null actionMode = null
clearCheckedItems() adapter.clearCheckedItems()
} }
} }
private fun clearCheckedItems(){
erroredDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
@ -331,20 +265,25 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) { when (direction) {
ItemTouchHelper.RIGHT -> { ItemTouchHelper.RIGHT -> {
runBlocking{ runBlocking{
downloadViewModel.queueDownloads(listOf(items[position])) downloadViewModel.reQueueDownloadItems(listOf(itemID))
} }
} }
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = items[position] lifecycleScope.launch {
downloadViewModel.deleteDownload(deletedItem) val deletedItem = withContext(Dispatchers.IO){
Snackbar.make(erroredRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) downloadViewModel.getItemByID(itemID)
.setAction(getString(R.string.undo)) { }
downloadViewModel.insert(deletedItem) downloadViewModel.deleteDownload(deletedItem.id)
}.show() Snackbar.make(erroredRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
downloadViewModel.insert(deletedItem)
}.show()
}
} }
} }
@ -383,7 +322,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
super.onChildDraw( super.onChildDraw(
c, c,
recyclerView, recyclerView,
viewHolder!!, viewHolder,
dX, dX,
dY, dY,
actionState, actionState,

View file

@ -48,6 +48,7 @@ import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@ -127,6 +128,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
) )
recyclerView = view.findViewById(R.id.recyclerviewhistorys) recyclerView = view.findViewById(R.id.recyclerviewhistorys)
recyclerView?.adapter = historyAdapter recyclerView?.adapter = historyAdapter
recyclerView?.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){

View file

@ -6,23 +6,15 @@ import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -36,26 +28,23 @@ import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.text.SimpleDateFormat import kotlinx.coroutines.withContext
import java.util.Calendar
import java.util.Locale
import java.util.UUID
class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener { class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -63,12 +52,11 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var queuedRecyclerView : RecyclerView private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : GenericDownloadAdapter private lateinit var adapter : GenericDownloadAdapter
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var fileSize: TextView private lateinit var fileSize: TextView
private var selectedObjects: ArrayList<DownloadItem>? = null private var totalSize: Int = 0
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem>
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -79,8 +67,6 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
activity = getActivity() activity = getActivity()
notificationUtil = NotificationUtil(requireContext()) notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
selectedObjects = arrayListOf()
return fragmentView return fragmentView
} }
@ -88,7 +74,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fileSize = view.findViewById(R.id.filesize) fileSize = view.findViewById(R.id.filesize)
queuedDownloads = adapter =
GenericDownloadAdapter( GenericDownloadAdapter(
this, this,
requireActivity() requireActivity()
@ -96,7 +82,9 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
queuedRecyclerView = view.findViewById(R.id.download_recyclerview) queuedRecyclerView = view.findViewById(R.id.download_recyclerview)
queuedRecyclerView.forceFastScrollMode() queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = queuedDownloads queuedRecyclerView.adapter = adapter
queuedRecyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback) val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -104,18 +92,34 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
} }
queuedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) queuedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.queuedDownloads.observe(viewLifecycleOwner) { lifecycleScope.launch {
items = it.toMutableList() downloadViewModel.queuedDownloads.collectLatest {
if (it.isEmpty()) fileSize.visibility = View.GONE adapter.submitData(it)
else{ }
val size = FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize }) }
if (size == "?") fileSize.visibility = View.GONE
else { adapter.addLoadStateListener { loadState ->
fileSize.visibility = View.VISIBLE lifecycleScope.launch {
fileSize.text = "${getString(R.string.file_size)}: ~ $size" if (loadState.append.endOfPaginationReached )
{
if ( adapter.itemCount < 1){
fileSize.visibility = View.GONE
}else{
val size = withContext(Dispatchers.IO){
FileUtil.convertFileSize(downloadViewModel.getQueuedCollectedFileSize())
}
if (size == "?") fileSize.visibility = View.GONE
else {
fileSize.visibility = View.VISIBLE
fileSize.text = "${getString(R.string.file_size)}: ~ $size"
}
}
} }
} }
queuedDownloads.submitList(it) }
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused)).observe(viewLifecycleOwner){
totalSize = it
} }
} }
@ -124,180 +128,104 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
} }
override fun onCardClick(itemID: Long) { override fun onCardClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
val bottomSheet = BottomSheetDialog(requireContext()) downloadViewModel.getItemByID(itemID)
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 { "`${requireContext().getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${requireContext().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(requireContext(), R.drawable.ic_music)
} }
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
}
}
val time = bottomSheet.findViewById<Chip>(R.id.time) UiUtil.showDownloadItemDetailsCard(
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note) item,
val container = bottomSheet.findViewById<Chip>(R.id.container_chip) requireActivity(),
val codec = bottomSheet.findViewById<Chip>(R.id.codec) DownloadRepository.Status.valueOf(item.status),
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size) removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE removeItem(it.id)
else { },
val calendar = Calendar.getInstance() downloadItem = {
calendar.timeInMillis = item.downloadStartTime downloadViewModel.deleteDownload(it.id)
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time) it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
time.setOnClickListener {
UiUtil.showDatePicker(parentFragmentManager) {
bottomSheet.dismiss()
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(item)
item.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking { runBlocking {
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {downloadItem ->
UiUtil.showDatePicker(parentFragmentManager) {
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
} }
} }
} )
} }
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 = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
bottomSheet.hide()
removeItem(itemID)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
else{
downloadNow!!.text = getString(R.string.download_now)
downloadNow.setOnClickListener {
bottomSheet.dismiss()
downloadViewModel.deleteDownload(item)
item.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(isChecked: Boolean, position: Int) {
val item = items.find { it.id == itemID } lifecycleScope.launch {
val now = System.currentTimeMillis() val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (isChecked) { val now = System.currentTimeMillis()
selectedObjects!!.add(item!!) if (isChecked) {
if (actionMode == null){ if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
}else{ actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" }
} }
} else {
else { actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
selectedObjects!!.remove(item) if (selectedObjects == 0){
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode?.finish()
if (selectedObjects!!.isEmpty()){ }
actionMode?.finish() }
if (actionMode != null){
actionMode!!.menu.getItem(2).isVisible = withContext(Dispatchers.IO){
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems, adapter.inverted, now)
}
actionMode!!.menu.getItem(1).isVisible = selectedObjects == 1 && position > 0
} }
}
if (actionMode != null){
actionMode!!.menu.getItem(1).isVisible = selectedObjects!!.all { it.downloadStartTime > now }
} }
} }
private fun removeItem(id: Long){ private fun removeItem(id: Long){
val item = items.find { it.id == id } ?: return lifecycleScope.launch {
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) val item = withContext(Dispatchers.IO){
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") downloadViewModel.getItemByID(id)
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
} }
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG) Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) { .setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item) downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.queueDownloads(listOf(item))
} }
}.show() }.show()
}
deleteDialog.show()
} }
deleteDialog.show()
} }
private val contextualActionBar = object : ActionMode.Callback { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu) mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}"
return true return true
} }
@ -318,46 +246,81 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){ lifecycleScope.launch {
val id = obj.id.toInt() val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
YoutubeDL.getInstance().destroyProcessById(id.toString()) withContext(Dispatchers.IO){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString()) downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
notificationUtil.cancelDownloadNotification(id) DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused))
downloadViewModel.deleteDownload(obj) }
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt())
downloadViewModel.deleteDownload(id)
}
actionMode?.finish()
} }
clearCheckedItems()
actionMode?.finish()
} }
deleteDialog.show() deleteDialog.show()
true true
} }
R.id.download -> { R.id.download -> {
for (obj in selectedObjects!!){ lifecycleScope.launch {
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(obj.id.toInt().toString()) val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
} withContext(Dispatchers.IO){
selectedObjects!!.forEach { it.downloadStartTime = 0L } downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
lifecycleScope.launch(Dispatchers.IO) { DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused))
downloadViewModel.queueDownloads(selectedObjects!!) }
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
for (id in selectedObjects){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
}
withContext(Dispatchers.IO) {
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
}
actionMode?.finish()
} }
true true
} }
R.id.select_all -> { R.id.select_all -> {
queuedDownloads.checkAll(items) adapter.checkAll()
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {
queuedDownloads.invertSelected(items) adapter.invertSelected()
val invertedList = arrayListOf<DownloadItem>() val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
items.forEach { actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (!selectedObjects?.contains(it)!!) invertedList.add(it) if (selectedObjects == 0) actionMode?.finish()
true
}
R.id.up -> {
lifecycleScope.launch {
if (adapter.getSelectedObjectsCount(totalSize) == 1){
val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtTopOfQueue(selectedObjects[0])
}
actionMode?.finish()
}
} }
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
else -> false else -> false
@ -366,14 +329,10 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null actionMode = null
clearCheckedItems() adapter.clearCheckedItems()
} }
} }
private fun clearCheckedItems(){
queuedDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
@ -383,12 +342,16 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = items[position] lifecycleScope.launch {
queuedDownloads.notifyItemChanged(position) val deletedItem = withContext(Dispatchers.IO){
removeItem(deletedItem.id) downloadViewModel.getItemByID(itemID)
}
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeItem(deletedItem.id)
}
} }
} }
@ -426,7 +389,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
super.onChildDraw( super.onChildDraw(
c, c,
recyclerView, recyclerView,
viewHolder!!, viewHolder,
dX, dX,
dY, dY,
actionState, actionState,

View file

@ -1,27 +1,21 @@
package com.deniscerri.ytdlnis.ui.downloads package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
@ -29,19 +23,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener { class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -49,11 +46,9 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var savedRecyclerView: RecyclerView private lateinit var savedRecyclerView: RecyclerView
private lateinit var savedDownloads: GenericDownloadAdapter private lateinit var adapter: GenericDownloadAdapter
private var selectedObjects: ArrayList<DownloadItem>? = null
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem> private var totalSize : Int = 0
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -61,16 +56,14 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
): View? { ): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false) fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
activity = getActivity() activity = getActivity()
selectedObjects = arrayListOf()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
return fragmentView return fragmentView
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
savedDownloads = adapter =
GenericDownloadAdapter( GenericDownloadAdapter(
this, this,
requireActivity() requireActivity()
@ -78,7 +71,8 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
savedRecyclerView = view.findViewById(R.id.download_recyclerview) savedRecyclerView = view.findViewById(R.id.download_recyclerview)
savedRecyclerView.forceFastScrollMode() savedRecyclerView.forceFastScrollMode()
savedRecyclerView.adapter = savedDownloads savedRecyclerView.enableFastScroll()
savedRecyclerView.adapter = adapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback) val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -87,148 +81,76 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
savedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) savedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.savedDownloads.observe(viewLifecycleOwner) { lifecycleScope.launch {
items = it.toMutableList() downloadViewModel.savedDownloads.collectLatest {
savedDownloads.submitList(it) adapter.submitData(it)
}
}
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Saved)).observe(viewLifecycleOwner){
totalSize = it
} }
} }
override fun onActionButtonClick(itemID: Long) { override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID } lifecycleScope.launch {
if (item != null){ val item = withContext(Dispatchers.IO){
runBlocking { downloadViewModel.getItemByID(itemID)
downloadViewModel.queueDownloads(listOf(item)) }
if (item != null){
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
}else{
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
} }
}else{
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
} }
} }
override fun onCardClick(itemID: Long) { override fun onCardClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
val bottomSheet = BottomSheetDialog(requireContext()) downloadViewModel.getItemByID(itemID)
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 { "`${requireContext().getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${requireContext().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(requireContext(), R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
} }
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
removeItem(it, sheet)
},
downloadItem = {it: DownloadItem ->
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = { it: DownloadItem ->
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {}
)
} }
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)
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 = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
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!!.text = getString(R.string.download)
download.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
download.tag = itemID
download.setOnClickListener{
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
bottomSheet.cancel()
}
download.setOnLongClickListener {
bottomSheet.cancel()
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(item), item.type, item, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
true
}
openFile.visibility = View.GONE
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(isChecked: Boolean, position: Int) {
val item = items.find { it.id == itemID } lifecycleScope.launch {
if (isChecked) { val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
selectedObjects!!.add(item!!) if (isChecked) {
if (actionMode == null){ if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{ }else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
}
} }
} else {
else { actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
selectedObjects!!.remove(item) if (selectedObjects == 0){
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode?.finish()
if (selectedObjects!!.isEmpty()){ }
actionMode?.finish()
} }
} }
} }
@ -240,7 +162,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
downloadViewModel.deleteDownload(item) downloadViewModel.deleteDownload(item.id)
} }
deleteDialog.show() deleteDialog.show()
} }
@ -249,7 +171,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
private val contextualActionBar = object : ActionMode.Callback { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.saved_downloads_menu_context, menu) mode!!.menuInflater.inflate(R.menu.saved_downloads_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}"
return true return true
} }
@ -270,39 +192,54 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){ lifecycleScope.launch {
downloadViewModel.deleteDownload(obj) val selectedObjects = if (adapter.inverted){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Saved))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
for (id in selectedObjects){
downloadViewModel.deleteDownload(id)
}
actionMode?.finish()
} }
clearCheckedItems()
actionMode?.finish()
} }
deleteDialog.show() deleteDialog.show()
true true
} }
R.id.download -> { R.id.download -> {
runBlocking { lifecycleScope.launch {
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList()) val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Saved))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
withContext(Dispatchers.IO) {
downloadViewModel.reQueueDownloadItems(selectedObjects)
}
actionMode?.finish() actionMode?.finish()
} }
true true
} }
R.id.select_all -> { R.id.select_all -> {
savedDownloads.checkAll(items) adapter.checkAll()
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {
savedDownloads.invertSelected(items) adapter.invertSelected()
val invertedList = arrayListOf<DownloadItem>() val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
items.forEach { actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (!selectedObjects?.contains(it)!!) invertedList.add(it) if (selectedObjects == 0) actionMode?.finish()
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
else -> false else -> false
@ -311,14 +248,10 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null actionMode = null
clearCheckedItems() adapter.clearCheckedItems()
} }
} }
private fun clearCheckedItems(){
savedDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
@ -328,18 +261,22 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = items[position] lifecycleScope.launch {
downloadViewModel.deleteDownload(deletedItem) val deletedItem = withContext(Dispatchers.IO){
Snackbar.make(savedRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) downloadViewModel.getItemByID(itemID)
.setAction(getString(R.string.undo)) { }
downloadViewModel.insert(deletedItem) downloadViewModel.deleteDownload(deletedItem.id)
}.show() Snackbar.make(savedRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
downloadViewModel.insert(deletedItem)
}.show()
}
} }
ItemTouchHelper.RIGHT -> { ItemTouchHelper.RIGHT -> {
onActionButtonClick(items[position].id) onActionButtonClick(itemID)
} }
} }
} }
@ -377,7 +314,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
super.onChildDraw( super.onChildDraw(
c, c,
recyclerView, recyclerView,
viewHolder!!, viewHolder,
dX, dX,
dY, dY,
actionState, actionState,

View file

@ -29,6 +29,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.CookieAdapter import com.deniscerri.ytdlnis.adapter.CookieAdapter
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
@ -68,6 +69,7 @@ class WebViewActivity : BaseActivity() {
generateBtn.setOnClickListener { generateBtn.setOnClickListener {
cookiesViewModel.getCookiesFromDB().getOrNull()?.let { cookiesViewModel.getCookiesFromDB().getOrNull()?.let {
Log.e("asas", it)
kotlin.runCatching { kotlin.runCatching {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){

View file

@ -31,28 +31,6 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
ignoreBatteryOptimization!!.isVisible = false ignoreBatteryOptimization!!.isVisible = false
} }
concurrentDownloads = findPreference("concurrent_downloads")
concurrentDownloads!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getString(R.string.warning))
.setMessage(resources.getString(R.string.workmanager_updated))
.setNegativeButton(resources.getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(resources.getString(R.string.restart)) { _, _ ->
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
requireContext().startActivity(intent)
if (context is Activity) {
(context as Activity).finish()
}
Runtime.getRuntime().exit(0)
}
.show()
true
}
ignoreBatteryOptimization = findPreference("ignore_battery") ignoreBatteryOptimization = findPreference("ignore_battery")
ignoreBatteryOptimization!!.onPreferenceClickListener = ignoreBatteryOptimization!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {

View file

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

View file

@ -201,7 +201,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
version = findPreference("version") version = findPreference("version")
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
version!!.summary = "${BuildConfig.VERSION_NAME} [${nativeLibraryDir?.split("/lib/")?.get(1)}] ${BuildConfig.BUILD_TYPE}" version!!.summary = "${BuildConfig.VERSION_NAME} [${nativeLibraryDir?.split("/lib/")?.get(1)}]"
version!!.onPreferenceClickListener = version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
@ -314,6 +314,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
} }
val arr = JsonArray() val arr = JsonArray()
items.forEach { items.forEach {
it.useAsExtraCommand = false
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
} }
return arr return arr

View file

@ -10,6 +10,7 @@ import android.os.storage.StorageVolume
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@ -20,6 +21,7 @@ import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.getBasePath import com.anggrayudi.storage.file.getBasePath
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -27,11 +29,13 @@ import okhttp3.internal.closeQuietly
import java.io.File import java.io.File
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.CopyOption
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
import java.util.* import java.util.*
import kotlin.io.path.absolutePathString
import kotlin.math.log10 import kotlin.math.log10
import kotlin.math.pow import kotlin.math.pow
@ -87,7 +91,7 @@ object FileUtil {
if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach
var destFile: DocumentFile var destFile: DocumentFile
try { try {
if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)".toRegex())){ if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)|(live_chat)".toRegex())){
it.delete() it.delete()
return@forEach return@forEach
} }
@ -182,8 +186,15 @@ object FileUtil {
super.onFailed(errorCode) super.onFailed(errorCode)
} }
override fun onConflict(
destinationFile: DocumentFile,
action: FileConflictAction
) {
action.confirmResolution(ConflictResolution.CREATE_NEW)
}
override fun onStart(file: Any, workerThread: Thread): Long { override fun onStart(file: Any, workerThread: Thread): Long {
return 1000 // update progress every 1 second return 500 // update progress every 1 second
} }
override fun onReport(report: Report) { override fun onReport(report: Report) {
@ -204,17 +215,37 @@ object FileUtil {
val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it) val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it)
for (ff in files){ for (ff in files){
val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}") val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}")
newFile.mkdirs() runCatching {
if (Build.VERSION.SDK_INT >= 26 ) { newFile.parentFile?.mkdirs()
Files.move( }
ff.toPath(), if (Build.VERSION.SDK_INT >= 26 ) {
newFile.toPath(), var newFileName = newFile.absolutePath
StandardCopyOption.REPLACE_EXISTING var counter = 1
) while (Files.exists(File(newFileName).toPath())) {
}else{ // If the file already exists in the destination directory, add a number to differentiate it
ff.renameTo(newFile) newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)")
counter++
}
fileList.add(Files.move(
ff.toPath(),
File(newFileName).toPath(),
StandardCopyOption.REPLACE_EXISTING
).absolutePathString())
ff.delete()
fileList.add(newFileName)
}else{
var newFileName = newFile.absolutePath
var counter = 1
while (File(newFileName).exists()) {
// If the file already exists in the destination directory, add a number to differentiate it
newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)")
counter++
}
ff.renameTo(File(newFileName))
fileList.add(newFileName)
} }
fileList.add(newFile.absolutePath)
} }
return@forEach return@forEach
} }
@ -238,17 +269,25 @@ object FileUtil {
return listOf(context.getString(R.string.unfound_file)) return listOf(context.getString(R.string.unfound_file))
} }
fun getCachePath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/.CACHE"
}
fun getDefaultAudioPath() : String{ fun getDefaultAudioPath() : String{
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Audio" return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/Audio"
} }
fun getDefaultVideoPath() : String{ fun getDefaultVideoPath() : String{
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Video" return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/Video"
} }
fun getDefaultCommandPath() : String { fun getDefaultCommandPath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Command" return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/Command"
}
fun getCookieFile(context : Context, path: (path: String) -> Unit){
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()) path(cookiesFile.absolutePath)
} }
fun convertFileSize(s: Long): String{ fun convertFileSize(s: Long): String{

View file

@ -448,14 +448,14 @@ class InfoUtil(private val context: Context) {
try { try {
val request = YoutubeDLRequest(url) val request = YoutubeDLRequest(url)
request.addOption("--print", "%(formats)s") request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(duration)s")
request.addOption("--skip-download") request.addOption("--skip-download")
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()) { request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
} }
@ -476,10 +476,10 @@ class InfoUtil(private val context: Context) {
return parseYTDLFormats(jsonArray) return parseYTDLFormats(jsonArray)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
Looper.prepare().run { Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
} }
e.printStackTrace()
} }
return emptyList() return emptyList()
} }
@ -507,9 +507,8 @@ class InfoUtil(private val context: Context) {
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()){ request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
} }
@ -569,9 +568,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()){ request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
} }
@ -611,7 +609,13 @@ class InfoUtil(private val context: Context) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
} }
} }
val url = jsonObject.getString("webpage_url")
val url = if (jsonObject.has("url")){
jsonObject.getString("url")
}else{
jsonObject.getString("webpage_url")
}
var thumb: String? = "" var thumb: String? = ""
if (jsonObject.has("thumbnail")) { if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail") thumb = jsonObject.getString("thumbnail")
@ -722,9 +726,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()){ request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
} }
@ -885,9 +888,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()){ request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
} }
@ -927,7 +929,7 @@ class InfoUtil(private val context: Context) {
} }
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val cacheDir = File(context.cacheDir.absolutePath + "/downloads/") val cacheDir = FileUtil.getCachePath()
val tempFileDir = File(cacheDir, downloadItem.id.toString()) val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete() tempFileDir.delete()
tempFileDir.mkdirs() tempFileDir.mkdirs()
@ -972,26 +974,29 @@ class InfoUtil(private val context: Context) {
} }
} }
if (sponsorBlockFilters.isNotEmpty()) {
val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() }) if (sharedPreferences.getBoolean("use_sponsorblock", true)){
if (filters.isNotBlank()) { if (sponsorBlockFilters.isNotEmpty()) {
request.addOption("--sponsorblock-remove", filters) val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
if (sharedPreferences.getBoolean("force_keyframes", false)){ if (filters.isNotBlank()) {
request.addOption("--force-keyframes-at-cuts") request.addOption("--sponsorblock-remove", filters)
if (sharedPreferences.getBoolean("force_keyframes", false)){
request.addOption("--force-keyframes-at-cuts")
}
} }
} }
} }
if(downloadItem.title.isNotBlank()){ if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title.take(200))) request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title.take(200)))
} }
if (downloadItem.author.isNotBlank()){ if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25))) request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.replace("%(uploader)s", downloadItem.author.take(25))
} }
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$","")) request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader|${downloadItem.author})s")
if (downloadItem.downloadSections.isNotBlank()){ if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach { downloadItem.downloadSections.split(";").forEach {
if (it.isBlank()) return@forEach if (it.isBlank()) return@forEach
@ -1004,7 +1009,7 @@ class InfoUtil(private val context: Context) {
request.addOption("--force-keyframes-at-cuts") request.addOption("--force-keyframes-at-cuts")
} }
} }
downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s" downloadItem.customFileNameTemplate += " %(section_title|)s %(autonumber)s"
request.addOption("--output-na-placeholder", " ") request.addOption("--output-na-placeholder", " ")
} }
@ -1013,7 +1018,7 @@ class InfoUtil(private val context: Context) {
} }
if (downloadItem.extraCommands.isNotBlank()){ if (downloadItem.extraCommands.isNotBlank()){
val conf = File(context.cacheDir.absolutePath + "/downloads/configExtraCommands${System.currentTimeMillis()}.txt") val conf = File(tempFileDir.absolutePath + "/${System.currentTimeMillis()}.txt")
conf.createNewFile() conf.createNewFile()
conf.writeText(downloadItem.extraCommands) conf.writeText(downloadItem.extraCommands)
request.addOption( request.addOption(
@ -1032,9 +1037,8 @@ class InfoUtil(private val context: Context) {
} }
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()){ request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
} }
@ -1093,7 +1097,7 @@ class InfoUtil(private val context: Context) {
if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg") if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg")
if (sharedPreferences.getBoolean("crop_thumbnail", true)){ if (sharedPreferences.getBoolean("crop_thumbnail", true)){
try { try {
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt") val config = File(tempFileDir.absolutePath + "/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData) config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
@ -1118,7 +1122,9 @@ class InfoUtil(private val context: Context) {
} }
if (downloadItem.videoPreferences.addChapters) { if (downloadItem.videoPreferences.addChapters) {
request.addOption("--sponsorblock-mark", "all") if (sharedPreferences.getBoolean("use_sponsorblock", true)){
request.addOption("--sponsorblock-mark", "all")
}
request.addOption("--embed-chapters") request.addOption("--embed-chapters")
} }
if (downloadItem.videoPreferences.embedSubs) { if (downloadItem.videoPreferences.embedSubs) {
@ -1146,10 +1152,18 @@ class InfoUtil(private val context: Context) {
} }
Log.e(DownloadWorker.TAG, formatArgument) Log.e(DownloadWorker.TAG, formatArgument)
request.addOption("-f", formatArgument) request.addOption("-f", formatArgument)
val outputFormat = downloadItem.container
if(outputFormat.isNotEmpty() && outputFormat != "Default" && outputFormat != context.getString(R.string.defaultValue) && supportedContainers.contains(outputFormat)){ if (downloadItem.format.container.equals("mpeg_4", true)) downloadItem.format.container = "mp4"
request.addOption("--merge-output-format", outputFormat.lowercase()) val outputContainer = downloadItem.container
if (outputFormat != "webm") { if(
outputContainer.isNotEmpty() &&
outputContainer != "Default" &&
outputContainer != context.getString(R.string.defaultValue) &&
supportedContainers.contains(outputContainer) &&
!outputContainer.equals(downloadItem.format.container, true)
){
request.addOption("--merge-output-format", outputContainer.lowercase())
if (outputContainer != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) { if (embedThumb) {
request.addOption("--embed-thumbnail") request.addOption("--embed-thumbnail")
@ -1188,7 +1202,7 @@ class InfoUtil(private val context: Context) {
DownloadViewModel.Type.command -> { DownloadViewModel.Type.command -> {
request.addOption( request.addOption(
"--config-locations", "--config-locations",
File(context.cacheDir.absolutePath + "/downloads/config${System.currentTimeMillis()}.txt").apply { File(FileUtil.getCachePath() + "/config[${downloadItem.id}].txt").apply {
writeText(downloadItem.format.format_note) writeText(downloadItem.format.format_note)
}.absolutePath }.absolutePath
) )

View file

@ -75,6 +75,25 @@ class NotificationUtil(var context: Context) {
return downloadNotificationBuilder return downloadNotificationBuilder
} }
fun createDefaultWorkerNotification() : Notification {
val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID)
return notificationBuilder
.setContentTitle(context.getString(R.string.downloading))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_launcher_foreground_large
)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions()
.build()
}
fun createDownloadServiceNotification( fun createDownloadServiceNotification(
pendingIntent: PendingIntent?, pendingIntent: PendingIntent?,
@ -188,6 +207,7 @@ class NotificationUtil(var context: Context) {
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )
.setContentText("")
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions() .clearActions()
@ -277,6 +297,11 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
} }
fun notify(id: Int, notification: Notification){
notificationManager.notify(id, notification)
}
fun updateDownloadNotification( fun updateDownloadNotification(
id: Int, id: Int,
desc: String, desc: String,
@ -344,6 +369,19 @@ class NotificationUtil(var context: Context) {
fun createYTDLUpdateNotification() : Notification{ fun createYTDLUpdateNotification() : Notification{
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
notificationBuilder.setContentTitle("Updating YT-DLP...") notificationBuilder.setContentTitle("Updating YT-DLP...")
.setOngoing(true)
.setCategory(Notification.CATEGORY_EVENT)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
android.R.drawable.stat_sys_download
)
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions()
return notificationBuilder.build() return notificationBuilder.build()
} }

View file

@ -7,9 +7,12 @@ import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.net.Uri import android.net.Uri
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
@ -28,18 +31,26 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.TemplateShortcut 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.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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog 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.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
@ -53,8 +64,11 @@ import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale
object UiUtil { object UiUtil {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -280,7 +294,7 @@ object UiUtil {
if (! file.exists()) return@forEach if (! file.exists()) return@forEach
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
fragmentContext, fragmentContext,
"com.deniscerri.ytdl.fileprovider", fragmentContext.packageName + ".fileprovider",
file file
) )
uris.add(uri) uris.add(uri)
@ -336,6 +350,150 @@ object UiUtil {
datePicker.show(fragmentManager, "datepicker") datePicker.show(fragmentManager, "datepicker")
} }
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){ fun showFormatDetails(format: Format, activity: Activity){
val bottomSheet = BottomSheetDialog(activity) val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -544,6 +702,395 @@ object UiUtil {
) )
} }
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)
embedSubs!!.isChecked = items.all { it.videoPreferences.embedSubs }
embedSubs.setOnClickListener {
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[0].videoPreferences.sponsorBlockFilters.contains(it) && items.size == 1) {
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 saveSubtitles = view.findViewById<Chip>(R.id.save_subtitles)
val subtitleLanguages = view.findViewById<Chip>(R.id.subtitle_languages)
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 { _, _ ->
if (saveSubtitles.isChecked) subtitleLanguages.visibility = View.VISIBLE
else subtitleLanguages.visibility = View.GONE
saveSubtitlesClicked(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[0].audioPreferences.sponsorBlockFilters.contains(it) && items.size == 1) {
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,
newTemplateClicked: () -> Unit,
editSelectedClicked: () -> 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()
}
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun RecyclerView.forceFastScrollMode() fun RecyclerView.forceFastScrollMode()
{ {
@ -561,15 +1108,27 @@ object UiUtil {
} }
} }
fun RecyclerView.enableFastScroll(){
val drawable = ShapeDrawable(OvalShape())
drawable.paint.color = context.getColor(android.R.color.transparent)
FastScrollerBuilder(this)
.setTrackDrawable(drawable)
.build()
}
fun EditText.setTextAndRecalculateWidth(t : String){ fun EditText.setTextAndRecalculateWidth(t : String){
val scale = context.resources.displayMetrics.density
this.setText(t) this.setText(t)
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
this.measure(widthMeasureSpec, heightMeasureSpec) this.measure(widthMeasureSpec, heightMeasureSpec)
val requiredWidth: Int = this.measuredWidth val requiredWidth: Int = this.measuredWidth
if (t.isEmpty()){ if (t.length < 5){
this.layoutParams.width = 70 this.layoutParams.width = (70 * scale + 0.5F).toInt()
}else{ }else{
this.layoutParams.width = requiredWidth this.layoutParams.width = requiredWidth
} }

View file

@ -3,27 +3,15 @@ package com.deniscerri.ytdlnis.util
import android.content.Context import android.content.Context
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.cronet.CronetDataSource
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.deniscerri.ytdlnis.App
import org.chromium.net.CronetEngine
import java.util.concurrent.Executors
object VideoPlayerUtil { object VideoPlayerUtil {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun buildPlayer(context: Context) : ExoPlayer { fun buildPlayer(context: Context) : ExoPlayer {
val player: ExoPlayer val player: ExoPlayer
val cronetEngine: CronetEngine = CronetEngine.Builder(App.instance)
.enableHttp2(true)
.enableQuic(true)
.enableBrotli(true)
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 1024L * 1024L) // 1MiB
.build()
val trackSelector = DefaultTrackSelector(context) val trackSelector = DefaultTrackSelector(context)
val loadControl = DefaultLoadControl.Builder() val loadControl = DefaultLoadControl.Builder()
@ -37,15 +25,8 @@ object VideoPlayerUtil {
) )
.build() .build()
val cronetDataSourceFactory = CronetDataSource.Factory(
cronetEngine,
Executors.newCachedThreadPool()
)
val dataSourceFactory = DefaultDataSource.Factory(context, cronetDataSourceFactory)
player = ExoPlayer.Builder(context) player = ExoPlayer.Builder(context)
.setUsePlatformDiagnostics(false) .setUsePlatformDiagnostics(false)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadControl) .setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)

View file

@ -7,10 +7,12 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.navigation.NavDeepLinkBuilder import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Data import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.Worker import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
@ -21,215 +23,236 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import kotlin.random.Random
class DownloadWorker( class DownloadWorker(
private val context: Context, private val context: Context,
workerParams: WorkerParameters workerParams: WorkerParameters
) : Worker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
override fun doWork(): Result { override suspend fun doWork(): Result {
itemId = inputData.getLong("id", 0) if (isStopped) return Result.success()
if (itemId == 0L || isStopped) return Result.success()
val notificationUtil = NotificationUtil(context) val notificationUtil = NotificationUtil(context)
val infoUtil = InfoUtil(context) val infoUtil = InfoUtil(context)
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao val dao = dbManager.downloadDao
val repository = DownloadRepository(dao)
val historyDao = dbManager.historyDao val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao) val logRepo = LogRepository(dbManager.logDao)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val time = System.currentTimeMillis() + 6000
val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time)
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success()
val downloadItem: DownloadItem? runningYTDLInstances.addAll(dao.getActiveDownloadsList().map { it.id })
try {
downloadItem = repository.getItemByID(itemId)
}catch (e: Exception){
e.printStackTrace()
return Result.success()
}
if (downloadItem.status != DownloadRepository.Status.Queued.toString() && downloadItem.status != DownloadRepository.Status.Paused.toString()) return Result.success()
val pendingIntent = NavDeepLinkBuilder(context) val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph) .setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment) .setDestination(R.id.downloadQueueMainFragment)
.createPendingIntent() .createPendingIntent()
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) var notification = notificationUtil.createDefaultWorkerNotification()
val foregroundInfo = ForegroundInfo(downloadItem.id.toInt(), notification) val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), notification)
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
Log.e(TAG, downloadItem.toString()) queuedItems.collectLatest { items ->
val running = ArrayList(runningYTDLInstances)
runBlocking{ if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
YoutubeDL.getInstance().destroyProcessById(itemId.toString()) val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
repository.setDownloadStatus(downloadItem, DownloadRepository.Status.Active) val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running }
} eligibleDownloads.forEach {downloadItem ->
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) runningYTDLInstances.add(downloadItem.id)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { withContext(Dispatchers.Main){
//update item if its incomplete notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
updateDownloadItem(downloadItem, infoUtil, dao, resultDao) notificationUtil.notify(downloadItem.id.toInt(), notification)
} }
val request = infoUtil.buildYoutubeDLRequest(downloadItem)
val request = infoUtil.buildYoutubeDLRequest(downloadItem) downloadItem.status = DownloadRepository.Status.Active.toString()
val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + downloadItem.id)
tempFileDir.delete()
tempFileDir.mkdirs()
val downloadLocation = downloadItem.downloadPath
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val logItem = LogItem(
0,
downloadItem.title.ifEmpty { downloadItem.url },
"Downloading:\n" +
"Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" +
"Format: ${downloadItem.format}\n\n" +
"Command: ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n",
downloadItem.format,
downloadItem.type,
System.currentTimeMillis(),
)
if (logDownloads){
runBlocking {
logItem.id = logRepo.insert(logItem)
downloadItem.logID = logItem.id
dao.update(downloadItem)
}
}
runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id))
val title: String = downloadItem.title
notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(),
line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
)
if (logDownloads){
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id) //update item if its incomplete
} updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
}
}
}.onSuccess {
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
runBlocking {
var finalPaths : List<String>?
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id))
try {
finalPaths = withContext(Dispatchers.IO){
FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id))
}
} }
if (finalPaths.isNotEmpty()){ val cacheDir = FileUtil.getCachePath()
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) val tempFileDir = File(cacheDir, downloadItem.id.toString())
}else{ tempFileDir.delete()
finalPaths = listOf(context.getString(R.string.unfound_file)) tempFileDir.mkdirs()
}
}catch (e: Exception){ val downloadLocation = downloadItem.downloadPath
finalPaths = listOf(context.getString(R.string.unfound_file)) val keepCache = sharedPreferences.getBoolean("keep_cache", false)
e.printStackTrace() val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() val logItem = LogItem(
}, 1000) 0,
} downloadItem.title.ifEmpty { downloadItem.url },
"Downloading:\n" +
"Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" +
if (downloadItem.type != DownloadViewModel.Type.command){
"Format: ${downloadItem.format}\n\n"
}
else { "" } +
"Command: ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n",
downloadItem.format,
downloadItem.type,
System.currentTimeMillis(),
)
//put download in history if (logDownloads){
val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) {
val unixtime = System.currentTimeMillis() / 1000
val file = File(finalPaths?.first()!!)
downloadItem.format.filesize = file.length()
val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id)
historyDao.insert(historyItem)
}
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished(
downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths,
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
)
if (wasQuickDownloaded){
runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id))
runBlocking { runBlocking {
infoUtil.getFromYTDL(downloadItem.url).forEach { res -> logItem.id = logRepo.insert(logItem)
if (res != null) { downloadItem.logID = logItem.id
resultDao.insert(res) dao.update(downloadItem)
}
}
runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id))
val title: String = downloadItem.title
notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(),
line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
)
if (logDownloads){
CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id)
} }
} }
} }
} }.onSuccess {
} runningYTDLInstances.remove(downloadItem.id)
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
runBlocking {
var finalPaths : List<String>?
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id))
try {
finalPaths = withContext(Dispatchers.IO){
FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id))
}
}
dao.delete(downloadItem.id) if (finalPaths.isNotEmpty()){
} setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id))
}else{
finalPaths = listOf(context.getString(R.string.unfound_file))
}
}catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file))
e.printStackTrace()
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
}.onFailure { //put download in history
if (it is YoutubeDL.CanceledException) { val incognito = sharedPreferences.getBoolean("incognito", false)
return Result.success( if (!incognito) {
Data.Builder().putString("output", "Download has been cancelled!").build() val unixtime = System.currentTimeMillis() / 1000
) val file = File(finalPaths?.first()!!)
}else{ downloadItem.format.filesize = file.length()
if (logDownloads){ val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id)
CoroutineScope(Dispatchers.IO).launch { historyDao.insert(historyItem)
if(it.message != null){ }
logRepo.update(it.message!!, logItem.id)
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished(
downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths,
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
)
if (wasQuickDownloaded){
runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id))
runBlocking {
infoUtil.getFromYTDL(downloadItem.url).forEach { res ->
if (res != null) {
resultDao.insert(res)
}
}
}
}
}
dao.delete(downloadItem.id)
if (logDownloads){
logRepo.update(it.out, logItem.id)
}
if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
} }
}.onFailure {
runningYTDLInstances.remove(downloadItem.id)
withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
}
if (it is YoutubeDL.CanceledException) {
}else{
if (logDownloads){
if(it.message != null){
logRepo.update(it.message!!, logItem.id)
}
}
tempFileDir.delete()
handler.postDelayed({
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
}, 1000)
Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
downloadItem.status = DownloadRepository.Status.Error.toString()
runBlocking {
dao.update(downloadItem)
}
notificationUtil.createDownloadErrored(
downloadItem.title, it.message,
if (logDownloads) logItem.id else null,
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
)
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id))
}
if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
} }
} }
}
tempFileDir.delete() if (eligibleDownloads.isNotEmpty()){
handler.postDelayed({ eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() }
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() dao.updateMultiple(eligibleDownloads)
}, 1000)
Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
downloadItem.status = DownloadRepository.Status.Error.toString()
runBlocking {
dao.update(downloadItem)
}
notificationUtil.createDownloadErrored(
downloadItem.title, it.message,
if (logDownloads) logItem.id else null,
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
)
return Result.success(
Data.Builder().putString("output", it.toString()).build()
)
} }
} }
return Result.success() return Result.success()
} }
@ -242,7 +265,7 @@ class DownloadWorker(
var wasQuickDownloaded = false var wasQuickDownloaded = false
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){ if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
runCatching { runCatching {
if (isStopped) destroyYTDLProcess() if (isStopped) YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
setProgressAsync(workDataOf("progress" to 0, "output" to context.getString(R.string.updating_download_data), "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to 0, "output" to context.getString(R.string.updating_download_data), "id" to downloadItem.id))
val info = infoUtil.getMissingInfo(downloadItem.url) val info = infoUtil.getMissingInfo(downloadItem.url)
if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString() if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString()
@ -259,17 +282,10 @@ class DownloadWorker(
return wasQuickDownloaded return wasQuickDownloaded
} }
override fun onStopped() {
destroyYTDLProcess()
super.onStopped()
}
private fun destroyYTDLProcess(){
YoutubeDL.getInstance().destroyProcessById(itemId.toInt().toString())
}
companion object { companion object {
var itemId: Long = 0 val runningYTDLInstances: MutableList<Long> = mutableListOf()
const val TAG = "DownloadWorker" const val TAG = "DownloadWorker"
} }

View file

@ -8,11 +8,11 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
@ -21,6 +21,7 @@ import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.TerminalActivity import com.deniscerri.ytdlnis.ui.more.TerminalActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
@ -29,17 +30,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.Calendar
class TerminalDownloadWorker( class TerminalDownloadWorker(
private val context: Context, private val context: Context,
workerParams: WorkerParameters workerParams: WorkerParameters
) : Worker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
override fun doWork(): Result { override suspend fun doWork(): Result {
val itemId = inputData.getInt("id", 0) itemId = inputData.getInt("id", 0)
val command = inputData.getString("command") val command = inputData.getString("command")
if (itemId == 0) return Result.failure() if (itemId == 0) return Result.failure()
if (command!!.isEmpty()) return Result.failure() if (command!!.isEmpty()) return Result.failure()
@ -49,6 +47,7 @@ class TerminalDownloadWorker(
val notificationUtil = NotificationUtil(context) val notificationUtil = NotificationUtil(context)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val infoUtil = InfoUtil(context)
val intent = Intent(context, TerminalActivity::class.java) val intent = Intent(context, TerminalActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
@ -59,27 +58,26 @@ class TerminalDownloadWorker(
val request = YoutubeDLRequest(emptyList()) val request = YoutubeDLRequest(emptyList())
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val downloadLocation = sharedPreferences.getString("command_path", context.getString(R.string.command_path)) val downloadLocation = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + itemId)
tempFileDir.delete()
tempFileDir.mkdirs()
val outputFile = File(context.cacheDir.absolutePath + "/$itemId.txt") val outputFile = File(context.cacheDir.absolutePath + "/$itemId.txt")
if (! outputFile.exists()) outputFile.createNewFile() if (! outputFile.exists()) outputFile.createNewFile()
request.addOption( request.addOption(
"--config-locations", "--config-locations",
File(context.cacheDir.absolutePath + "/downloads/config${System.currentTimeMillis()}.txt").apply { File(context.cacheDir.absolutePath + "/config${System.currentTimeMillis()}.txt").apply {
writeText(command) writeText(command)
}.absolutePath }.absolutePath
) )
val cookiesFile = File(context.cacheDir, "cookies.txt") if (sharedPreferences.getBoolean("use_cookies", false)){
if (cookiesFile.exists()){ val cookiesFile = File(context.cacheDir, "cookies.txt")
request.addOption("--cookies", cookiesFile.absolutePath) if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath)
}
} }
request.addOption("-P", tempFileDir.absolutePath) request.addOption("-P", FileUtil.getDefaultCommandPath() + "/" + itemId)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false) val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
@ -88,7 +86,7 @@ class TerminalDownloadWorker(
"Terminal Download", "Terminal Download",
"Downloading:\n" + "Downloading:\n" +
"Terminal Download\n" + "Terminal Download\n" +
"Command: ${command}\n\n", "Command: ${infoUtil.parseYTDLRequestString(request)}\n\n",
Format(), Format(),
DownloadViewModel.Type.command, DownloadViewModel.Type.command,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -119,7 +117,7 @@ class TerminalDownloadWorker(
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
//move file from internal to set download directory //move file from internal to set download directory
try { try {
FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation!!, false){ p -> FileUtil.moveFile(File(FileUtil.getDefaultCommandPath() + "/" + itemId),context, downloadLocation!!, false){ p ->
setProgressAsync(workDataOf("progress" to p)) setProgressAsync(workDataOf("progress" to p))
} }
}catch (e: Exception){ }catch (e: Exception){
@ -130,12 +128,10 @@ class TerminalDownloadWorker(
} }
} }
if (it.out.length > 200){ outputFile.appendText("${it.out}\n")
outputFile.appendText("${it.out}\n") if (logDownloads){
if (logDownloads){ CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { logRepo.update(it.out, logItem.id)
logRepo.update(it.out, logItem.id)
}
} }
} }
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
@ -144,7 +140,7 @@ class TerminalDownloadWorker(
if (it is YoutubeDL.CanceledException) { if (it is YoutubeDL.CanceledException) {
return Result.failure() return Result.failure()
} }
outputFile.appendText("${it.message}\n") outputFile.appendText("\n${it.message}\n")
if (logDownloads){ if (logDownloads){
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (it.message != null){ if (it.message != null){
@ -152,7 +148,7 @@ class TerminalDownloadWorker(
} }
} }
} }
tempFileDir.delete() File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
Log.e(TAG, context.getString(R.string.failed_download), it) Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
@ -163,12 +159,9 @@ class TerminalDownloadWorker(
return Result.success() return Result.success()
} }
override fun onStopped() {
YoutubeDL.getInstance().destroyProcessById(DownloadWorker.itemId.toInt().toString())
super.onStopped()
}
companion object { companion object {
private var itemId : Int = 0
const val TAG = "DownloadWorker" const val TAG = "DownloadWorker"
} }

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:textColorPrimary" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M23,16c0,1.1 -0.9,2 -2,2h-1l-2,-2h3L21,4L6,4L4,2h17c1.1,0 2,0.9 2,2v12zM17.5,18l-2,-2zM14.9,18l6,6 1.3,-1.3 -4.7,-4.7 -2,-2L1.2,1.8 0,3.1l1,1L1,16c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h0.9zM3,16L3,6.1l9.9,9.9L3,16z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M20,3H4C2.9,3 2,3.9 2,5v10c0,1.1 0.9,2 2,2h6v2H8v2h8v-2h-2v-2h6c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3"/>
</vector>

View file

@ -1,5 +1,5 @@
<vector android:height="24dp" <vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:textColorPrimary" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> <path android:fillColor="?android:colorAccent" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector> </vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="@color/icon_bg"
android:pathData="M 0 0 H 192 V 192 H 0 V 0 Z" />
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<group android:scaleX="0.58"
android:scaleY="0.58"
android:translateX="5.04"
android:translateY="5.04">
<path
android:fillColor="@android:color/white"
android:pathData="M20,4H4C2.89,4 2,4.9 2,6v12c0,1.1 0.89,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.11,4 20,4zM20,18H4V8h16V18zM18,17h-6v-2h6V17zM7.5,17l-1.41,-1.41L8.67,13l-2.59,-2.59L7.5,9l4,4L7.5,17z"/>
</group>
</vector>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<TextView
android:id="@+id/incognito_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/incognito"
android:textStyle="bold"
android:translationZ="20dp"
android:visibility="gone"
android:padding="5dp"
android:background="?colorPrimaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/incognito_header">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frame_layout"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bottomNavigationView"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/bottomNavigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:labelVisibilityMode="selected"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,312 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<com.google.android.material.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/search_hint"
app:layout_anchor="@id/search_bar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ScrollView
android:id="@+id/search_history_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="wrap_content"
android:scrollbars="none"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:id="@+id/providers"
android:layout_margin="10dp"
app:chipSpacingVertical="-10dp"
android:paddingEnd="20dp"
android:paddingStart="0dp"
app:singleSelection="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true" />
</HorizontalScrollView>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
style="@style/Divider.Vertical"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/queries_constraint"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:id="@+id/queries"
android:layout_margin="10dp"
app:chipSpacingVertical="-10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@+id/init_search_query"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:singleLine="false" />
<com.google.android.material.button.MaterialButton
android:id="@+id/init_search_query"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:iconSize="20dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-10dp"
android:visibility="gone"
android:id="@+id/link_you_copied"
layout="@layout/search_suggestion_item"
/>
<LinearLayout
android:id="@+id/search_history_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
</ScrollView>
<ScrollView
android:id="@+id/search_suggestions_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/search_suggestions_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</LinearLayout>
</com.google.android.material.search.SearchView>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/home_appbarlayout"
app:liftOnScroll="true"
android:background="@null"
android:elevation="0dp"
android:fitsSystemWindows="true"
app:elevation="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
app:layout_scrollFlags="scroll"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/home_toolbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/app_name" />
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="250dp">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="250dp">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="250dp">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/home_fabs"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/download_all_coordinator"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="match_parent">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
android:text="@string/download_all"
app:icon="@drawable/ic_down"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/download_selected_coordinator"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="match_parent">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:paddingBottom="20dp"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/frame_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintWidth_percent=".5"
app:layout_constraintHorizontal_bias="0.0"
app:cardCornerRadius="0dp"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/info"
app:layout_constraintTop_toTopOf="parent">
<androidx.media3.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:resize_mode="fixed_height"
app:useDefaultControls="true"
app:use_controller="true" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/download_thumb"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_image"
app:iconSize="30dp"
app:iconTint="?android:colorAccent"
app:layout_constraintTop_toTopOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:layout_marginHorizontal="10dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:singleLine="false"
android:text="@string/app_name"
android:textSize="17sp"
app:layout_constraintEnd_toStartOf="@+id/download_music"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/frame_layout" />
<TextView
android:id="@+id/bottom_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:ellipsize="end"
app:layout_constraintVertical_bias="0.0"
android:singleLine="false"
android:text="@string/app_name"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download_music"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/download_music"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
android:layout_width="55dp"
android:layout_marginTop="10dp"
android:layout_height="55dp"
android:layout_marginHorizontal="10dp"
app:icon="@drawable/ic_music"
app:layout_constraintEnd_toStartOf="@+id/download_video"
app:layout_constraintTop_toBottomOf="@id/frame_layout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/download_video"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
android:layout_width="55dp"
android:layout_marginTop="10dp"
android:layout_height="55dp"
android:layout_marginHorizontal="10dp"
app:icon="@drawable/ic_video"
app:layout_constraintEnd_toEndOf="@id/frame_layout"
app:layout_constraintTop_toBottomOf="@id/frame_layout" />
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@string/app_name"
android:textAlignment="textStart"
android:textSize="15sp"
app:icon="@drawable/ic_link"
app:layout_constraintEnd_toStartOf="@+id/info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/download_music" />
<androidx.core.widget.NestedScrollView
android:id="@+id/info"
android:orientation="vertical"
android:layout_width="0dp"
android:layout_marginHorizontal="10dp"
android:scrollbars="none"
app:layout_constraintWidth_percent=".5"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintStart_toEndOf="@id/frame_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<include layout="@layout/history_no_results"
android:visibility="gone" />
<TextView
android:id="@+id/running"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="10dp"
android:text="@string/running"
android:textStyle="bold"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/running_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/queued"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="10dp"
android:text="@string/download_queue"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queued_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -157,11 +157,6 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="100dp"
app:spanCount="3" app:spanCount="3"

View file

@ -171,12 +171,6 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="100dp"
app:spanCount="2" app:spanCount="2"

View file

@ -156,11 +156,6 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="100dp"
app:spanCount="4" app:spanCount="4"

View file

@ -0,0 +1,126 @@
<LinearLayout android:layout_width="match_parent"
android:id="@+id/adjust"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:paddingTop="10dp"
android:layout_width="wrap_content"
android:paddingBottom="5dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/adjust_audio" />
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:chipSpacingVertical="0dp"
app:singleLine="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/embed_thumb"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/embed_thumb"/>
<com.google.android.material.chip.Chip
android:id="@+id/split_by_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/split_by_chapters"/>
<com.google.android.material.chip.Chip
android:id="@+id/cut"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_cut"
android:text="@string/cut"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:chipSpacingVertical="0dp"
app:singleLine="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/sponsorblock_filters"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/sponsorblock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/filename_template"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/file_name_template"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_edit"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/extra_commands"
android:visibility="gone"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_extra_commands"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_terminal"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View file

@ -0,0 +1,45 @@
<LinearLayout
android:id="@+id/adjust"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingVertical="10dp"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="wrap_content"
android:paddingBottom="5dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/adjust_templates" />
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:selectionRequired="false"
app:singleLine="false"
app:chipSpacingVertical="0dp"
app:singleSelection="false">
<com.google.android.material.chip.Chip
android:id="@+id/newTemplate"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_template"
app:chipIcon="@drawable/ic_plus" />
<com.google.android.material.chip.Chip
android:id="@+id/editSelected"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit_selected"
app:chipIcon="@drawable/ic_edit" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>

View file

@ -0,0 +1,205 @@
<LinearLayout
android:id="@+id/adjust"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:paddingHorizontal="10dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:paddingTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:text="@string/adjust_video"
android:textSize="15sp" />
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingVertical="0dp"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/embed_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/embed_subtitles" />
<com.google.android.material.chip.Chip
android:id="@+id/add_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/add_chapter" />
<com.google.android.material.chip.Chip
android:id="@+id/save_thumbnail"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/save_thumb" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/remove_audio"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/remove_audio"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/split_by_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/split_by_chapters"/>
<com.google.android.material.chip.Chip
android:id="@+id/cut"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_cut"
android:minWidth="30dp"
android:text="@string/cut"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/sponsorblock_filters"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/sponsorblock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/filename_template"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/file_name_template"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_edit"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/save_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/save_subs"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/subtitle_languages"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:checked="false"
android:text="@string/subtitle_languages"/>
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/extra_commands"
android:visibility="gone"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_extra_commands"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_terminal"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View file

@ -319,6 +319,16 @@
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/force_keyframes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:checked="true"
android:textStyle="bold"
android:textSize="15sp"
android:text="@string/force_keyframes"/>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -183,7 +183,6 @@
android:minHeight="0dp" android:minHeight="0dp"
android:padding="0dp" android:padding="0dp"
app:cornerRadius="10dp" app:cornerRadius="10dp"
app:icon="@drawable/ic_video"
app:iconTint="?attr/colorAccent" app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View file

@ -92,11 +92,6 @@
android:paddingTop="10dp" android:paddingTop="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:paddingBottom="70dp" android:paddingBottom="70dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

@ -104,6 +104,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="monospace" android:fontFamily="monospace"
android:textSize="14sp"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:maxLines="2000" /> android:maxLines="2000" />

View file

@ -5,12 +5,13 @@
<LinearLayout android:layout_width="match_parent" <LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="20dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:text="@string/format" android:text="@string/format"
android:textSize="14sp" android:textSize="17sp"
android:layout_margin="20dp" android:layout_margin="20dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -22,22 +23,24 @@
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:weightSum="100" android:weightSum="100"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:text="@string/format_id" android:text="@string/format_id"
android:textSize="18sp" android:textSize="14sp"
android:layout_margin="10dp" android:textStyle="bold"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/format_id_value" android:id="@+id/format_id_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -51,25 +54,26 @@
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:weightSum="100" android:weightSum="100"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50" android:textStyle="bold"
android:textSize="14sp"
android:text="@string/url" android:text="@string/url"
android:textSize="18sp" android:layout_marginHorizontal="10dp"
android:layout_margin="10dp" android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/format_url_value" android:id="@+id/format_url_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:maxLines="1"
android:ellipsize="end"
android:layout_weight="50"
android:textSize="18sp"
android:layout_margin="10dp"
android:gravity="end" android:gravity="end"
android:maxLines="2"
android:ellipsize="end"
android:textSize="18sp"
android:layout_marginHorizontal="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
@ -83,22 +87,23 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50" android:textStyle="bold"
android:textSize="18sp" android:textSize="14sp"
android:text="@string/container" android:text="@string/container"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/container_value" android:id="@+id/container_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -112,22 +117,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50" android:textStyle="bold"
android:textSize="18sp" android:textSize="14sp"
android:text="@string/codec" android:text="@string/codec"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/codec_value" android:id="@+id/codec_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -141,22 +146,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50" android:textStyle="bold"
android:textSize="14sp"
android:text="@string/file_size" android:text="@string/file_size"
android:textSize="18sp" android:layout_marginHorizontal="10dp"
android:layout_margin="10dp" android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/filesize_value" android:id="@+id/filesize_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -170,22 +175,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:textStyle="bold"
android:textSize="14sp"
android:text="@string/quality" android:text="@string/quality"
android:layout_weight="50" android:layout_marginHorizontal="10dp"
android:textSize="18sp" android:layout_marginVertical="5dp"
android:layout_margin="10dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/format_note_value" android:id="@+id/format_note_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -199,22 +204,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50" android:textStyle="bold"
android:textSize="14sp"
android:text="FPS" android:text="FPS"
android:textSize="18sp" android:layout_marginHorizontal="10dp"
android:layout_margin="10dp" android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/fps_value" android:id="@+id/fps_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -228,22 +233,22 @@
android:weightSum="100" android:weightSum="100"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50" android:textStyle="bold"
android:textSize="14sp"
android:text="@string/audio_samplerate" android:text="@string/audio_samplerate"
android:textSize="18sp" android:layout_marginHorizontal="10dp"
android:layout_margin="10dp" android:layout_marginVertical="5dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/asr_value" android:id="@+id/asr_value"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="50"
android:textSize="18sp" android:textSize="18sp"
android:layout_margin="10dp" android:layout_marginHorizontal="10dp"
android:gravity="end" android:gravity="end"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>

View file

@ -9,7 +9,7 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/pause_resume" android:id="@+id/pause_resume"
android:visibility="visible" android:visibility="gone"
style="@style/Widget.Material3.Button.TextButton" style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -110,131 +110,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<LinearLayout <include layout="@layout/adjust_audio" />
android:layout_width="match_parent"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:paddingTop="10dp"
android:layout_width="wrap_content"
android:paddingBottom="5dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/adjust_audio" />
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:chipSpacingVertical="0dp"
app:singleLine="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/embed_thumb"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/embed_thumb"/>
<com.google.android.material.chip.Chip
android:id="@+id/split_by_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/split_by_chapters"/>
<com.google.android.material.chip.Chip
android:id="@+id/cut"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_cut"
android:text="@string/cut"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:chipSpacingVertical="0dp"
app:singleLine="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/sponsorblock_filters"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/sponsorblock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/filename_template"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/file_name_template"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_edit"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/extra_commands"
android:visibility="gone"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_extra_commands"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_terminal"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -69,50 +69,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<include layout="@layout/adjust_command" />
<LinearLayout
android:layout_width="wrap_content"
android:paddingVertical="10dp"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:layout_width="wrap_content"
android:paddingBottom="5dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/adjust_templates" />
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:selectionRequired="false"
app:singleLine="false"
app:chipSpacingVertical="0dp"
app:singleSelection="false">
<com.google.android.material.chip.Chip
android:id="@+id/newTemplate"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_template"
app:chipIcon="@drawable/ic_plus" />
<com.google.android.material.chip.Chip
android:id="@+id/editSelected"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit_selected"
app:chipIcon="@drawable/ic_edit" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -110,209 +110,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<include layout="@layout/adjust_video" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="10dp">
<TextView
android:paddingTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:text="@string/adjust_video"
android:textSize="15sp" />
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingVertical="0dp"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/embed_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/embed_subtitles" />
<com.google.android.material.chip.Chip
android:id="@+id/add_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/add_chapter" />
<com.google.android.material.chip.Chip
android:id="@+id/save_thumbnail"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/save_thumb" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/remove_audio"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/remove_audio"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/split_by_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/split_by_chapters"/>
<com.google.android.material.chip.Chip
android:id="@+id/cut"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_cut"
android:minWidth="30dp"
android:text="@string/cut"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/sponsorblock_filters"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/sponsorblock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/filename_template"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/file_name_template"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_edit"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:scrollbars="none"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/save_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/save_subs"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/subtitle_languages"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:checked="false"
android:text="@string/subtitle_languages"/>
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/extra_commands"
android:visibility="gone"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_extra_commands"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_terminal"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -112,11 +112,6 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="100dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">

View file

@ -133,14 +133,8 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="100dp"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/> />

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
@ -23,14 +24,7 @@
android:id="@+id/download_recyclerview" android:id="@+id/download_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:fastScrollEnabled="true"
android:scrollbars="none" android:scrollbars="none"
android:paddingBottom="100dp"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -38,5 +32,5 @@
</androidx.recyclerview.widget.RecyclerView> </androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>

View file

@ -13,7 +13,8 @@
<FrameLayout <FrameLayout
android:id="@+id/download_card_view" android:id="@+id/download_card_view"
android:layout_width="0dp" android:layout_width="0dp"
android:padding="10dp" android:paddingHorizontal="10dp"
android:paddingVertical="3dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardPreventCornerOverlap="true" app:cardPreventCornerOverlap="true"
@ -32,7 +33,7 @@
android:clickable="false" android:clickable="false"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHorizontal_weight="0.3" app:layout_constraintHorizontal_weight="0.4"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -81,11 +82,9 @@
android:maxLines="2" android:maxLines="2"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:scrollbars="none" android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="4" android:shadowDx="4"
android:shadowDy="4" android:shadowDy="4"
android:shadowRadius="2" android:shadowRadius="2"
android:textColor="@color/white"
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -93,6 +92,22 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
/> />
<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingHorizontal="10dp"
android:scrollbars="none"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -39,22 +39,6 @@
app:layout_constraintTop_toTopOf="@+id/frame_layout" app:layout_constraintTop_toTopOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="@+id/frame_layout" /> app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pause"
android:visibility="gone"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_play_arrow_white"
android:layout_width="wrap_content"
android:indeterminate="true"
android:translationZ="10dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:orientation="vertical" android:orientation="vertical"
@ -72,13 +56,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginVertical="10dp"> android:layout_margin="10dp">
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_marginStart="20dp" android:textStyle="bold"
android:layout_marginEnd="10dp" android:layout_marginHorizontal="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="2"
@ -93,14 +77,14 @@
<TextView <TextView
android:id="@+id/bottom_info" android:id="@+id/bottom_info"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginHorizontal="10dp"
android:ellipsize="end" android:ellipsize="end"
app:layout_constraintVertical_bias="0.0"
android:singleLine="false" android:singleLine="false"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toTopOf="@+id/nestedScrollView"
app:layout_constraintEnd_toStartOf="@+id/download_music" app:layout_constraintEnd_toStartOf="@+id/download_music"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -136,6 +120,7 @@
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -150,12 +135,12 @@
<TextView <TextView
android:id="@+id/running" android:id="@+id/running"
android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="10dp" android:layout_marginVertical="10dp"
android:layout_marginHorizontal="10dp"
android:text="@string/running" android:text="@string/running"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -170,12 +155,12 @@
<TextView <TextView
android:id="@+id/queued" android:id="@+id/queued"
android:layout_marginHorizontal="20dp"
android:textStyle="bold"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="10dp" android:layout_marginVertical="10dp"
android:layout_marginHorizontal="10dp"
android:text="@string/download_queue" android:text="@string/download_queue"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/running_recycler" /> app:layout_constraintTop_toBottomOf="@id/running_recycler" />
@ -189,13 +174,11 @@
app:layout_constraintTop_toBottomOf="@id/queued" /> app:layout_constraintTop_toBottomOf="@id/queued" />
<Button <Button
android:id="@+id/bottom_sheet_link" android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon" style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/app_name" android:text="@string/app_name"

View file

@ -23,12 +23,37 @@
app:menu="@menu/select_playlist_menu" app:menu="@menu/select_playlist_menu"
android:layout_gravity="bottom"> android:layout_gravity="bottom">
<TextView <com.google.android.material.textfield.TextInputLayout
android:id="@+id/selected" android:id="@+id/from_textinput"
android:layout_width="wrap_content" style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:textStyle="bold" android:layout_width="70dp"
android:layout_marginHorizontal="30dp" android:layout_height="wrap_content"
android:layout_height="wrap_content" /> android:layout_marginStart="40dp"
android:layout_marginEnd="10dp"
android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:hint="@string/end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.bottomappbar.BottomAppBar> </com.google.android.material.bottomappbar.BottomAppBar>
@ -36,12 +61,10 @@
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/check_all" android:id="@+id/check_all"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:text="@string/select_all"
app:icon="@drawable/ic_select_all" app:icon="@drawable/ic_select_all"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_anchor="@id/bottomAppBar"/> app:layout_anchor="@id/bottomAppBar"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -56,72 +79,42 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" android:orientation="horizontal">
android:orientation="horizontal"
android:paddingTop="20dp">
<TextView <com.google.android.material.appbar.AppBarLayout
android:id="@+id/bottom_sheet_title" android:id="@+id/appbar"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/select"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bottomsheet_ok"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
android:layout_height="wrap_content"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
app:menu="@menu/playlist_download_menu"
android:layout_height="?attr/actionBarSize"
android:paddingEnd="16dp"
android:paddingStart="6dp"
app:contentInsetStartWithNavigation="0dp"
app:navigationIcon="@drawable/baseline_close_24" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottomsheet_ok" app:layout_constraintTop_toBottomOf="@+id/appbar"
app:layout_constraintVertical_bias="0.0"> app:layout_constraintVertical_bias="0.0">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout> </LinearLayout>
@ -136,11 +129,6 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="80dp" android:paddingBottom="80dp"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

@ -16,12 +16,6 @@
android:icon="@drawable/ic_format" android:icon="@drawable/ic_format"
app:showAsAction="always"/> app:showAsAction="always"/>
<item
android:id="@+id/filename_template"
android:title="@string/file_name_template"
android:icon="@drawable/if_file_rename"
app:showAsAction="always"/>
<item <item
android:id="@+id/folder" android:id="@+id/folder"
android:title="@string/directories" android:title="@string/directories"
@ -29,5 +23,12 @@
app:showAsAction="always"/> app:showAsAction="always"/>
<item
android:id="@+id/more"
android:title="@string/more"
android:icon="@drawable/ic_more"
app:showAsAction="always"/>
</menu> </menu>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:theme="@style/BaseTheme">
<item android:title="@string/download"
app:showAsAction="always"
android:id="@+id/downloadPlaylist"/>
</menu>

View file

@ -9,12 +9,19 @@
android:icon="@drawable/baseline_delete_24" android:icon="@drawable/baseline_delete_24"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/up"
android:icon="@drawable/ic_arrow_up"
app:showAsAction="ifRoom"
android:title="Move Up" />
<item <item
android:id="@+id/download" android:id="@+id/download"
android:title="@string/download" android:title="@string/download"
android:icon="@drawable/baseline_download_24" android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/select_all" android:id="@+id/select_all"
android:title="@string/select_all" android:title="@string/select_all"

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background_dark"/> <background android:drawable="@drawable/ic_launcher_background_dark"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background_light"/> <background android:drawable="@drawable/ic_launcher_background_light"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/terminal_icon_background"/>
<foreground android:drawable="@drawable/terminal_icon_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/terminal_icon_background"/>
<foreground android:drawable="@drawable/terminal_icon_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -9,7 +9,11 @@
android:id="@+id/homeFragment" android:id="@+id/homeFragment"
android:name="com.deniscerri.ytdlnis.ui.HomeFragment" android:name="com.deniscerri.ytdlnis.ui.HomeFragment"
android:label="@string/app_name" android:label="@string/app_name"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home" >
<action
android:id="@+id/action_homeFragment_to_resultCardDetailsDialog"
app:destination="@id/resultCardDetailsDialog" />
</fragment>
<fragment <fragment
android:id="@+id/historyFragment" android:id="@+id/historyFragment"
android:name="com.deniscerri.ytdlnis.ui.downloads.HistoryFragment" android:name="com.deniscerri.ytdlnis.ui.downloads.HistoryFragment"
@ -80,4 +84,8 @@
android:id="@+id/downloadLogFragment" android:id="@+id/downloadLogFragment"
android:name="com.deniscerri.ytdlnis.ui.more.downloadLogs.DownloadLogFragment" android:name="com.deniscerri.ytdlnis.ui.more.downloadLogs.DownloadLogFragment"
android:label="DownloadLogFragment" /> android:label="DownloadLogFragment" />
<fragment
android:id="@+id/resultCardDetailsDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
android:label="ResultCardDetailsDialog" />
</navigation> </navigation>

View file

@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="audio_containers"> <string-array name="audio_containers">
<item>@string/defaultValue</item> <item>@string/defaultValue</item>
<item>mp3</item> <item>mp3</item>
@ -65,12 +65,14 @@
<string-array name="format_ordering"> <string-array name="format_ordering">
<item>@string/file_size</item> <item>@string/file_size</item>
<item>@string/container</item> <item>@string/container</item>
<item>@string/codec</item>
<item>ID</item> <item>ID</item>
</string-array> </string-array>
<string-array name="format_ordering_values"> <string-array name="format_ordering_values">
<item>filesize</item> <item>filesize</item>
<item>container</item> <item>container</item>
<item>codec</item>
<item>id</item> <item>id</item>
</string-array> </string-array>
@ -770,4 +772,22 @@
<item>queue</item> <item>queue</item>
</string-array> </string-array>
<string-array name="video_codec">
<item>@string/defaultValue</item>
<item tools:ignore="Typos">AV1</item>
<item>VP9</item>
<item tools:ignore="Typos">AVC</item>
<item>H265</item>
<item>H264</item>
</string-array>
<string-array name="video_codec_values">
<item></item>
<item>av0</item>
<item>vp</item>
<item>avc</item>
<item>hev</item>
<item>h264</item>
</string-array>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show more