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">
<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>

View file

@ -2,15 +2,15 @@ plugins {
id 'com.android.application'
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10"
}
def properties = new Properties()
def versionMajor = 1
def versionMinor = 6
def versionPatch = 3
def versionBuild = 17 // bump for dogfood builds, public betas, etc.
def versionPatch = 5
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = ""
if (versionBuild > 0){
@ -49,10 +49,8 @@ android {
useSupportLibrary true
}
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
@ -76,6 +74,8 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
signingConfig signingConfigs.debug
applicationIdSuffix '.beta'
versionNameSuffix '-Beta'
}
debug {
@ -98,7 +98,7 @@ android {
disable 'MissingTranslation'
}
android.applicationVariants.all { variant ->
android.applicationVariants.configureEach { variant ->
variant.outputs.each { output ->
def baseAbiVersionCode = project.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
if (baseAbiVersionCode != null) {
@ -149,7 +149,7 @@ dependencies {
implementation 'com.google.android.material:material:1.9.0-rc01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
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-ui-ktx:$navVer"
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-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"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
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-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 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
@ -200,7 +202,7 @@ dependencies {
implementation("androidx.media3:media3-ui:$media3_version")
// For RTSP playback support with ExoPlayer
implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version")
implementation("androidx.media3:media3-datasource-cronet:$media3_version")
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" />
</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
android:name=".receiver.ShareFileActivity"
@ -96,6 +107,8 @@
android:name=".terminalShareAlias"
android:targetActivity=".ui.more.TerminalActivity"
android:enabled="false"
android:icon="@mipmap/terminal_icon"
android:roundIcon="@mipmap/terminal_icon_round"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
@ -204,7 +217,7 @@
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.deniscerri.ytdl.fileprovider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:enabled="true"
android:grantUriPermissions="true">
@ -212,18 +225,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</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>
</manifest>

View file

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

View file

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

View file

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

View file

@ -13,16 +13,9 @@ import java.lang.reflect.Type
class Converters {
@TypeConverter
fun stringToListOfFormats(value: String?): ArrayList<Format> {
val listType: Type = object : TypeToken<ArrayList<Format?>?>() {}.type
return Gson().fromJson(value, listType)
}
fun stringToListOfFormats(value: String) = Gson().fromJson(value, Array<Format>::class.java).toMutableList()
@TypeConverter
fun listOfFormatsToString(list: ArrayList<Format?>?): String {
val gson = Gson()
return gson.toJson(list)
}
fun listOfFormatsToString(list: List<Format?>?) = Gson().toJson(list).toString()
@TypeConverter
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)
@TypeConverter
fun stringToListOfChapters(value: String?): ArrayList<ChapterItem>? {
val listType: Type = object : TypeToken<ArrayList<ChapterItem?>?>() {}.type
return Gson().fromJson(value, listType)
}
fun stringToListOfChapters(value: String?) = Gson().fromJson(value, Array<ChapterItem>::class.java).toMutableList()
@TypeConverter
fun listOfChaptersToString(list: ArrayList<ChapterItem?>?): String {
fun listOfChaptersToString(list: List<ChapterItem?>?): String {
val gson = Gson()
return gson.toJson(list)
}

View file

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

View file

@ -1,20 +1,26 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import kotlinx.coroutines.flow.Flow
@Dao
interface DownloadDao {
@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'")
fun getActiveDownloads() : Flow<List<DownloadItem>>
@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'")
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'")
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")
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")
fun getQueuedDownloadsList() : List<DownloadItem>
@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")
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")
fun getErroredDownloads() : Flow<List<DownloadItem>>
fun getErroredDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem>
@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")
fun getDownloadById(id: Long) : DownloadItem
@ -52,6 +73,22 @@ interface DownloadDao {
@Query("SELECT status FROM downloads WHERE id=:id")
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)
suspend fun insert(item: DownloadItem) : Long
@ -73,7 +110,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Saved'")
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()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -82,6 +119,9 @@ interface DownloadDao {
@Upsert
suspend fun update(item: DownloadItem)
@Upsert
fun updateMultiple(items :List<DownloadItem>)
@Query("SELECT * FROM downloads ORDER BY id DESC LIMIT 1")
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")
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>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(new: SearchHistoryItem)
suspend fun insert(newItem: SearchHistoryItem)
@Query("DELETE FROM searchHistory")
suspend fun deleteAll()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,8 +3,13 @@ package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver
import android.content.Context
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.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PauseDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
@ -16,6 +21,14 @@ class PauseDownloadNotificationReceiver : BroadcastReceiver() {
notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString())
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.ui.BaseActivity
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.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -95,10 +95,9 @@ class ShareActivity : BaseActivity() {
askPermissions()
val action = intent.action
val type = intent.clipData
Log.e("aa", intent.toString())
if (Intent.ACTION_SEND == action && type != null) {
if (intent.getStringExtra(Intent.EXTRA_TEXT) == null){
if (Intent.ACTION_SEND == action || Intent.ACTION_VIEW == action) {
if (intent.getStringExtra(Intent.EXTRA_TEXT) == null && Intent.ACTION_SEND == action){
intent.setClass(this, MainActivity::class.java)
startActivity(intent)
finishAffinity()
@ -107,7 +106,7 @@ class ShareActivity : BaseActivity() {
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)
loadingBottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -118,7 +117,11 @@ class ShareActivity : BaseActivity() {
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 {
try {
val result = resultViewModel.getItemByURL(inputQuery)
@ -166,7 +169,7 @@ class ShareActivity : BaseActivity() {
private fun showSelectPlaylistItems(it: List<ResultItem?>){
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")
}else{
lifecycleScope.launch(Dispatchers.IO){

View file

@ -6,6 +6,8 @@ import android.content.*
import android.content.Context.CLIPBOARD_SERVICE
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.os.Bundle
import android.os.Handler
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.util.InfoUtil
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.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
@ -55,6 +59,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.util.*
@ -146,6 +151,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
recyclerView = view.findViewById(R.id.recyclerViewHome)
recyclerView?.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
recyclerView?.adapter = homeAdapter
recyclerView?.enableFastScroll()
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
resultViewModel.items.observe(viewLifecycleOwner) {
@ -251,12 +257,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
override fun onResume() {
super.onResume()
if(arguments?.getString("url") == null){
resultViewModel.checkTrending()
if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){
resultViewModel.checkTrending()
}else{
resultViewModel.loadingItems.postValue(true)
}
}else{
arguments
arguments?.remove("url")
arguments?.remove("showDownloadsWithUpdatedFormats")
}
arguments?.remove("showDownloadsWithUpdatedFormats")
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
lifecycleScope.launch {
updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
@ -533,7 +542,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
resultViewModel.deleteAll()
lifecycleScope.launch(Dispatchers.IO){
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())){
val resultItem = downloadViewModel.createEmptyResultItem(queryList.first())
if (sharedPreferences!!.getBoolean("download_card", true)) {

View file

@ -37,7 +37,7 @@ import java.util.*
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 sharedPreferences: SharedPreferences
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 templates = view.findViewById<Button>(R.id.commands)
val shortcuts = view.findViewById<Button>(R.id.shortcuts)
val currentCommand = infoUtil.parseYTDLRequestString(infoUtil.buildYoutubeDLRequest(item))
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
@ -111,7 +116,10 @@ class AddExtraCommandsDialog(private val item: DownloadItem, private val callbac
highlight.setSpan(currentText)
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
text!!.postDelayed({
text.setSelection(text.length())

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.Color
import android.net.Uri
@ -15,6 +16,7 @@ import android.view.View
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem.fromUri
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.MergingMediaSource
import androidx.media3.ui.PlayerView
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
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.color.MaterialColors
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.textfield.TextInputLayout
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 cancelBtn : Button
private lateinit var okBtn : Button
private lateinit var forceKeyframes: MaterialSwitch
private lateinit var suggestedChips: ChipGroup
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)
resetBtn = view.findViewById(R.id.reset_all)
chipGroup = view.findViewById(R.id.cut_list_chip_group)
forceKeyframes = view.findViewById(R.id.force_keyframes)
selectedCuts = if (chipGroup.childCount == 0){
@ -152,7 +158,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
if (urls.isNullOrEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url)
}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) {}
}
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")
@ -253,31 +268,25 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
fromTextInput.editText!!.setTextAndRecalculateWidth("0:00")
toTextInput.editText!!.setTextAndRecalculateWidth(item.duration)
rangeSlider.addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: RangeSlider) {
rangeSlider.setOnTouchListener { v, event -> // Handle touch events here
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) {
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) {}
}
})
rangeSlider.setOnDragListener { view, dragEvent ->
updateFromSlider()
false
}
fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener {
@ -304,8 +313,10 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
startValue = 0F
}
fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString())
rangeSlider.setValues(startValue, endValue)
okBtn.isEnabled = startValue != 0F && endValue != 100F
okBtn.isEnabled = startValue != 0F || endValue != 100F
try {
player.seekTo(seconds.toLong() * 1000)
player.play()
@ -341,12 +352,14 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
if (seconds == 0) {
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(toTextInput.editText!!.text.toString())
rangeSlider.setValues(startValue, endValue)
okBtn.isEnabled = startValue != 0F && endValue != 100F
okBtn.isEnabled = startValue != 0F || endValue != 100F
try {
player.seekTo(startSeconds.toLong() * 1000)
player.play()
@ -370,6 +383,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
okBtn.isEnabled = false
okBtn.setOnClickListener {
forceKeyframes.isVisible = true
val chip = createChip("${fromTextInput.editText!!.text}-${toTextInput.editText!!.text}")
chip.performClick()
cutSection.visibility = View.GONE
@ -379,6 +393,26 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
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(){
if (chapters!!.isEmpty()) suggestedChapters.visibility = View.GONE
else {
@ -391,6 +425,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
chip.isCheckedIconVisible = false
suggestedChips.addView(chip)
chip.setOnClickListener { c ->
forceKeyframes.isVisible = true
val createdChip = createChapterChip(it, null)
createdChip.performClick()
cutSection.visibility = View.GONE
@ -424,6 +459,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
}
if (item.downloadSections.isNotBlank()){
forceKeyframes.isVisible = true
chipGroup.removeAllViews()
item.downloadSections.split(";").forEachIndexed { _, it ->
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 = ""
}
val embedThumb = view.findViewById<Chip>(R.id.embed_thumb)
embedThumb!!.isChecked = downloadItem.audioPreferences.embedThumb
embedThumb.setOnClickListener {
downloadItem.audioPreferences.embedThumb = embedThumb.isChecked
}
val splitByChapters = view.findViewById<Chip>(R.id.split_by_chapters)
if (downloadItem.downloadSections.isNotBlank()){
splitByChapters.isEnabled = false
splitByChapters.isChecked = false
}else{
splitByChapters!!.isChecked = downloadItem.audioPreferences.splitByChapters
}
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 ->
UiUtil.configureAudio(
view,
requireActivity(),
listOf(downloadItem),
embedThumbClicked = {
downloadItem.audioPreferences.embedThumb = it
},
splitByChaptersClicked = {
downloadItem.audioPreferences.splitByChapters = it
},
filenameTemplateSet = {
downloadItem.customFileNameTemplate = it
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.audioPreferences.sponsorBlockFilters.clear()
for (i in 0 until checkedItems.size) {
if (checkedItems[i]) {
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
}
}
}
// 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 {
},
cutClicked = {cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
}
}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 {
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c
@ -358,9 +249,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
}else{
extraCommands.visibility = View.GONE
}
)
}catch (e : Exception){
e.printStackTrace()
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics
@ -73,8 +74,9 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = 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.peekHeight = displayMetrics.heightPixels
}
}

View file

@ -151,43 +151,43 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace
))
val newTemplate : Chip = view.findViewById(R.id.newTemplate)
newTemplate.setOnClickListener {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
populateCommandTemplates(templates, autoCompleteTextView)
UiUtil.configureCommand(
view,
1,
newTemplateClicked = {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
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) {
e.printStackTrace()
}

View file

@ -6,6 +6,7 @@ import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
@ -39,6 +40,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
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.Format
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.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -63,6 +66,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.lang.reflect.Type
import java.text.SimpleDateFormat
import java.util.Locale
@ -98,8 +102,9 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = 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.peekHeight = displayMetrics.heightPixels
}
}
@ -112,8 +117,11 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = listAdapter
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
recyclerView.enableFastScroll()
if(sharedPreferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
listAdapter.submitList(items.toList())
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 ->
when (m.itemId) {
R.id.folder -> {
@ -264,6 +274,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
listAdapter.notifyDataSetChanged()
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel()
}
@ -273,6 +284,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
listAdapter.notifyDataSetChanged()
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel()
}
@ -285,6 +297,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
listAdapter.notifyDataSetChanged()
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 30
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 -> {
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
}
@ -507,8 +604,12 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
items[i] = item
if (! items.all { it.type == items[0].type }){
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}else{
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){
@ -581,7 +682,7 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
super.onChildDraw(
c,
recyclerView,
viewHolder!!,
viewHolder,
dX,
dY,
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)
embedSubs!!.isChecked = downloadItem.videoPreferences.embedSubs
embedSubs.setOnClickListener {
downloadItem.videoPreferences.embedSubs = embedSubs.isChecked
}
val addChapters = view.findViewById<Chip>(R.id.add_chapters)
addChapters!!.isChecked = downloadItem.videoPreferences.addChapters
addChapters.setOnClickListener{
downloadItem.videoPreferences.addChapters = addChapters.isChecked
}
val splitByChapters = view.findViewById<Chip>(R.id.split_by_chapters)
if(downloadItem.downloadSections.isNotBlank()){
splitByChapters.isEnabled = false
splitByChapters.isChecked = false
}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 ->
UiUtil.configureVideo(
view,
requireActivity(),
listOf(downloadItem),
embedSubsClicked = {
downloadItem.videoPreferences.embedSubs = it
},
addChaptersClicked = {
downloadItem.videoPreferences.addChapters = it
},
splitByChaptersClicked = {
downloadItem.videoPreferences.splitByChapters = it
},
saveThumbnailClicked = {
downloadItem.SaveThumb = it
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.videoPreferences.sponsorBlockFilters.clear()
for (i in 0 until checkedItems.size) {
for (i in checkedItems.indices) {
if (checkedItems[i]) {
downloadItem.videoPreferences.sponsorBlockFilters.add(values[i])
}
}
}
// 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 {
},
cutClicked = { cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
}
}else{
cut.isEnabled = false
}
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)
) { _: 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 {
},
filenameTemplateSet = {
downloadItem.customFileNameTemplate = it
},
saveSubtitlesClicked = {
downloadItem.videoPreferences.writeSubs = it
},
subtitleLanguagesClicked = {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
downloadItem.videoPreferences.subsLanguages = it
}
},
removeAudioClicked = {
downloadItem.videoPreferences.removeAudio = it
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c
@ -417,9 +275,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
}else{
extraCommands.visibility = View.GONE
}
)
} catch (e: Exception) {
e.printStackTrace()
}

View file

@ -51,7 +51,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var view: View
enum class FormatSorting {
filesize, container, id
filesize, container, codec, id
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -127,7 +127,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
refreshBtn.setOnClickListener {
lifecycleScope.launch {
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) {
listener.onContinueOnBackground(items)
this@FormatSelectionBottomSheetDialog.dismiss()
@ -158,7 +158,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
formats = listOf(res)
//playlist format filtering
//list format filtering
}else{
var progress = "0/${items.size}"
formatCollection.clear()
@ -168,8 +168,8 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
lifecycleScope.launch(Dispatchers.Main){
progress = "${formatCollection.size}/${items.size}"
refreshBtn.text = progress
formatCollection.add(it)
}
formatCollection.add(it)
}
}
formats = formatCollection
@ -242,6 +242,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val finalFormats: List<Format> = when(sortBy){
FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value }
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
}

View file

@ -6,6 +6,7 @@ import android.app.DownloadManager
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
@ -28,6 +29,7 @@ import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.ui.PlayerView
import androidx.navigation.fragment.findNavController
import androidx.paging.filter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
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.ActiveDownloadMinifiedAdapter
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.repository.DownloadRepository
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.UiUtil
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.BottomSheetDialogFragment
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 it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.FieldPosition
import java.text.SimpleDateFormat
import java.util.*
@ -73,15 +76,14 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var activeDownloads: ActiveDownloadMinifiedAdapter
private lateinit var queuedDownloads: GenericDownloadAdapter
private lateinit var activeAdapter: ActiveDownloadMinifiedAdapter
private lateinit var queuedAdapter: GenericDownloadAdapter
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 sharedPreferences: SharedPreferences
private lateinit var dialogView : View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -89,8 +91,6 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
notificationUtil = NotificationUtil(requireActivity().applicationContext)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
queuedItems = listOf()
activeItems = listOf()
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
}
@ -101,7 +101,8 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
savedInstanceState: Bundle?
): View {
// 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 {
@ -116,14 +117,27 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null)
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)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activeDownloads = ActiveDownloadMinifiedAdapter(this,requireActivity())
queuedDownloads = GenericDownloadAdapter(this,requireActivity())
activeAdapter = ActiveDownloadMinifiedAdapter(this,requireActivity())
queuedAdapter = GenericDownloadAdapter(this,requireActivity())
val bottomSheetLink = view.findViewById<MaterialButton>(R.id.bottom_sheet_link)
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 queued = view.findViewById<TextView>(R.id.queued)
runningRecycler.adapter = activeDownloads
runningRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
runningRecycler.adapter = activeAdapter
runningRecycler.layoutManager = GridLayoutManager(context, 1)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
@ -146,56 +160,57 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
itemTouchHelper.attachToRecyclerView(queuedRecycler)
}
queuedRecycler.adapter = queuedDownloads
queuedRecycler.adapter = queuedAdapter
queuedRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
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
runningRecycler.visibility = View.GONE
}else{
running.visibility = View.VISIBLE
runningRecycler.visibility = View.VISIBLE
}
}
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
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 progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(progress, true)
}catch (ignored: Exception) {}
}
}
}
val progress = work.progress.getInt("progress", 0)
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(progress, true)
}catch (ignored: Exception) {}
}
}
}
activeItems = it
activeDownloads.submitList(it)
lifecycleScope.launch {
downloadViewModel.queuedDownloads.map { it.filter { d -> d.url == item.url } }.collectLatest {
queuedAdapter.submitData(it)
}
}
downloadViewModel.queuedDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
queued.visibility = View.GONE
queuedRecycler.visibility = View.GONE
}else{
queued.visibility = View.VISIBLE
queuedRecycler.visibility = View.VISIBLE
queuedAdapter.addLoadStateListener { loadState ->
lifecycleScope.launch {
if (loadState.append.endOfPaginationReached )
{
if (queuedAdapter.itemCount < 1){
queued.visibility = View.GONE
queuedRecycler.visibility = View.GONE
}else{
queued.visibility = View.VISIBLE
queuedRecycler.visibility = View.VISIBLE
}
}
}
queuedItems = it
queuedDownloads.submitList(it)
}
downloadViewModel.activeDownloadsCount.observe(viewLifecycleOwner){
@ -238,23 +253,9 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
videoView = view.findViewById(R.id.video_view)
val pause = view.findViewById<MaterialButton>(R.id.pause)
val player = VideoPlayerUtil.buildPlayer(requireContext())
videoView.player = player
videoView.setOnClickListener {
if (player.isPlaying){
player.pause()
pause.visibility = View.VISIBLE
}
else {
player.play()
pause.visibility = View.GONE
}
}
lifecycleScope.launch {
try {
val data : MutableList<String?> = withContext(Dispatchers.IO){
@ -324,25 +325,29 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
}
private fun removeQueuedItem(id: Long){
val item = queuedItems.find { it.id == id } ?: return
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)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
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)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
Snackbar.make(requireView().rootView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
}
deleteDialog.show()
}
deleteDialog.show()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
@ -353,12 +358,16 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
val deletedItem = queuedItems[position]
queuedDownloads.notifyItemChanged(position)
removeQueuedItem(deletedItem.id)
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
queuedAdapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeQueuedItem(deletedItem.id)
}
}
}
@ -396,7 +405,7 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
super.onChildDraw(
c,
recyclerView,
viewHolder!!,
viewHolder,
dX,
dY,
actionState,
@ -410,133 +419,135 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
}
override fun onCardClick(itemID: Long) {
val item = queuedItems.find { it.id == itemID } ?: return
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)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
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)
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.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
else formatNote!!.text = item.format.format_note
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)
if (item.format.container != "") container!!.text = item.format.container.uppercase()
else container!!.visibility = View.GONE
time.setOnClickListener {
UiUtil.showDatePicker(parentFragmentManager) {
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()
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(item)
item.downloadStartTime = it.timeInMillis
downloadViewModel.deleteDownload(item.id)
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
)
}
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) {
lifecycleScope.launch {
@ -557,41 +568,45 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
cancelActiveDownload(itemID)
}
override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) {
val item = activeItems.find { it.id == itemID } ?: return
when(action){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
lifecycleScope.launch {
cancelItem(itemID.toInt())
item.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
}
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch {
item.status = DownloadRepository.Status.Active.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
when(action){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
lifecycleScope.launch {
cancelItem(itemID.toInt())
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)
else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item)
list
}
val queue = if (activeCount > 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)
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
}
}
}
override fun onCardClick() {
this.dismiss()
@ -607,15 +622,19 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
}
private fun cancelActiveDownload(itemID: Long){
val id = itemID.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id)
lifecycleScope.launch {
val id = itemID.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id)
activeItems.find { it.id == itemID }?.let {
it.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(it)
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
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
import android.annotation.SuppressLint
import android.app.ActionBar.LayoutParams
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
@ -8,9 +9,12 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
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.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
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.bottomsheet.BottomSheetBehavior
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.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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 resultViewModel: ResultViewModel
private lateinit var listAdapter : PlaylistAdapter
private lateinit var recyclerView: RecyclerView
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var selectedText: TextView
private lateinit var ok: MenuItem
private lateinit var toolbar: MaterialToolbar
private lateinit var fromTextInput: TextInputLayout
private lateinit var toTextInput: TextInputLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.FullScreenDialogTheme)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
}
@SuppressLint("RestrictedApi")
override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.select_playlist_items, null)
dialog.setContentView(view)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.select_playlist_items, container, false)
}
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
toolbar = view.findViewById<MaterialToolbar>(R.id.toolbar)
listAdapter =
PlaylistAdapter(
@ -70,10 +86,10 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = listAdapter
recyclerView.enableFastScroll()
listAdapter.submitList(items)
selectedText = view.findViewById<Button>(R.id.selected)
selectedText.text = "0 ${resources.getString(R.string.selected)}"
toolbar.title = "0 ${resources.getString(R.string.selected)}"
fromTextInput = view.findViewById(R.id.from_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 (endNr > items.size) endNr = items.size - 1
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 ->
reset()
val start = fromTextInput.editText!!.text.toString()
val end = _text.toString()
@ -113,7 +129,8 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
if (startNr <= 0) startNr = 0
if (endNr > items.size) endNr = items.size -1
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()
fromTextInput.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{
reset()
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
ok.isEnabled = false
fromTextInput.editText!!.setTextAndRecalculateWidth("")
toTextInput.editText!!.setTextAndRecalculateWidth("")
}
}
val ok = view.findViewById<Button>(R.id.bottomsheet_ok)
ok!!.setOnClickListener {
ok = toolbar.menu.getItem(0)
ok.isEnabled = false
ok.setOnMenuItemClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
@ -164,6 +184,7 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
dismiss()
}
true
}
view.findViewById<BottomAppBar>(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem ->
@ -172,17 +193,22 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){
selectedText.text = resources.getString(R.string.all_items_selected)
toolbar.title= resources.getString(R.string.all_items_selected)
}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){
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
}
ok.isEnabled = checkedItems.isNotEmpty()
}
true
}
toolbar.setNavigationOnClickListener {
requireActivity().finishAffinity()
}
}
override fun onCancel(dialog: DialogInterface) {
@ -209,21 +235,24 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
}
private fun reset(){
ok.isEnabled = false
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>) {
if (checkedItems.size == items.size){
selectedText.text = resources.getString(R.string.all_items_selected)
toolbar.title = resources.getString(R.string.all_items_selected)
}else{
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
toolbar.title = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}
if (checkedItems.isEmpty()){
ok.isEnabled = false
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
}else{
ok.isEnabled = true
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
}

View file

@ -10,9 +10,11 @@ import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
@ -39,7 +41,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private lateinit var activeDownloads : ActiveDownloadAdapter
lateinit var downloadItem: DownloadItem
private lateinit var notificationUtil: NotificationUtil
private lateinit var list: List<DownloadItem>
private lateinit var pauseResume: MaterialButton
private lateinit var workManager: WorkManager
@ -53,7 +54,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
activity = getActivity()
notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
list = listOf()
workManager = WorkManager.getInstance(requireContext())
return fragmentView
}
@ -77,6 +77,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
if (pauseResume.text == requireContext().getString(R.string.pause)){
lifecycleScope.launch {
pauseResume.isEnabled = false
val queued = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
@ -92,18 +93,28 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager.cancelAllWorkByTag(it.id.toString())
}
list.forEach {
val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
active.forEach {
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it)
}
activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true
}
}else{
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 {
it.status = DownloadRepository.Status.Active.toString()
downloadViewModel.updateDownload(it)
@ -116,17 +127,40 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
runBlocking {
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) {
list = it
activeDownloads.submitList(it)
if (it.size > 1){
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)
}else{
pauseResume.text = requireContext().getString(R.string.pause)
@ -135,37 +169,16 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
activeDownloads.notifyDataSetChanged()
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) {
lifecycleScope.launch {
if (list.size == 1){
val count = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.count()
if (count == 1){
val queue = withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
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) {
val item = list.find { it.id == itemID } ?: return
when(action){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
lifecycleScope.launch {
cancelItem(itemID.toInt())
item.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeDownloads.notifyItemChanged(position)
}
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
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)
else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item)
list
when(action){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
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)
runBlocking {
downloadViewModel.queueDownloads(queue)
val count = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.count()
val queue = if (count > 1) listOf(item)
else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item)
list
}
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
}
}
}
}
override fun onOutputClick(item: DownloadItem) {
@ -238,17 +260,19 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
private fun cancelDownload(itemID: Long){
cancelItem(itemID.toInt())
list.find { it.id == itemID }?.let {
it.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(it)
lifecycleScope.launch {
cancelItem(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
}
}
}
override fun onClick(p0: View?) {
TODO("Not yet implemented")
}
}

View file

@ -1,28 +1,21 @@
package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
@ -30,20 +23,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
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.databinding.FragmentHomeBinding
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.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
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.withContext
class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -51,10 +46,9 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var cancelledRecyclerView : RecyclerView
private lateinit var cancelledDownloads : GenericDownloadAdapter
private var selectedObjects: ArrayList<DownloadItem>? = null
private lateinit var adapter : GenericDownloadAdapter
private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem>
private var totalSize = 0
override fun onCreateView(
inflater: LayoutInflater,
@ -63,16 +57,14 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
activity = getActivity()
selectedObjects = arrayListOf()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cancelledDownloads =
adapter =
GenericDownloadAdapter(
this,
requireActivity()
@ -80,7 +72,8 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
cancelledRecyclerView = view.findViewById(R.id.download_recyclerview)
cancelledRecyclerView.forceFastScrollMode()
cancelledRecyclerView.adapter = cancelledDownloads
cancelledRecyclerView.adapter = adapter
cancelledRecyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -89,146 +82,74 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
cancelledRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.cancelledDownloads.observe(viewLifecycleOwner) {
items = it.toMutableList()
cancelledDownloads.submitList(it)
lifecycleScope.launch {
downloadViewModel.cancelledDownloads.collectLatest {
adapter.submitData(it)
}
}
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Cancelled)).observe(viewLifecycleOwner){
totalSize = it
}
}
override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID }
if (item != null){
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
lifecycleScope.launch {
runCatching {
val item = withContext(Dispatchers.IO){
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) {
val item = items.find { it.id == itemID } ?: return
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)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
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) {
val item = items.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
override fun onCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (isChecked) {
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
}
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
actionMode?.finish()
else {
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0){
actionMode?.finish()
}
}
}
}
@ -240,7 +161,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
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 ->
downloadViewModel.deleteDownload(item)
downloadViewModel.deleteDownload(item.id)
}
deleteDialog.show()
}
@ -249,7 +170,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
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
}
@ -270,39 +191,56 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
downloadViewModel.deleteDownload(obj)
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()
}
adapter.clearCheckedItems()
for (id in selectedObjects){
downloadViewModel.deleteDownload(id)
}
actionMode?.finish()
}
clearCheckedItems()
actionMode?.finish()
}
deleteDialog.show()
true
}
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 {
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList())
actionMode?.finish()
}
true
}
R.id.select_all -> {
cancelledDownloads.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
true
}
R.id.invert_selected -> {
cancelledDownloads.invertSelected(items)
val invertedList = arrayListOf<DownloadItem>()
items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
adapter.invertSelected()
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0) actionMode?.finish()
true
}
else -> false
@ -311,15 +249,10 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
clearCheckedItems()
adapter.clearCheckedItems()
}
}
private fun clearCheckedItems(){
cancelledDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
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) {
val position = viewHolder.bindingAdapterPosition
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
val deletedItem = items[position]
downloadViewModel.deleteDownload(deletedItem)
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()
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
downloadViewModel.deleteDownload(deletedItem.id)
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 -> {
onActionButtonClick(items[position].id)
onActionButtonClick(itemID)
}
}
}

View file

@ -105,7 +105,10 @@ class DownloadQueueMainFragment : Fragment(){
initMenu()
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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
@ -31,19 +25,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
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.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
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.withContext
class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener, OnItemClickListener {
@ -51,10 +48,9 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var erroredRecyclerView : RecyclerView
private lateinit var erroredDownloads : GenericDownloadAdapter
private lateinit var items : MutableList<DownloadItem>
private var selectedObjects: ArrayList<DownloadItem>? = null
private lateinit var adapter : GenericDownloadAdapter
private var actionMode : ActionMode? = null
private var totalSize: Int = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -63,15 +59,13 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
selectedObjects = arrayListOf()
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
erroredDownloads =
adapter =
GenericDownloadAdapter(
this,
requireActivity()
@ -79,7 +73,8 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
erroredRecyclerView = view.findViewById(R.id.download_recyclerview)
erroredRecyclerView.forceFastScrollMode()
erroredRecyclerView.adapter = erroredDownloads
erroredRecyclerView.adapter = adapter
erroredRecyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -87,147 +82,77 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
}
erroredRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.erroredDownloads.observe(viewLifecycleOwner) {
items = it.toMutableList()
erroredDownloads.submitList(it)
lifecycleScope.launch {
downloadViewModel.erroredDownloads.collectLatest {
adapter.submitData(it)
}
}
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Error)).observe(viewLifecycleOwner){
totalSize = it
}
}
override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return
if (item.logID != null) {
val bundle = Bundle()
bundle.putLong("logID", item.logID!!)
findNavController().navigate(
R.id.downloadLogFragment,
bundle
)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
if (item.logID != null) {
val bundle = Bundle()
bundle.putLong("logID", item.logID!!)
findNavController().navigate(
R.id.downloadLogFragment,
bundle
)
}
}
}
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())
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)}`" }
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
removeItem(it, sheet)
// 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)
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
)
},
downloadItem = {
runBlocking{
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {}
)
}
}
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = items.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
override fun onCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (isChecked) {
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
}else{
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
}
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
actionMode?.finish()
else {
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0){
actionMode?.finish()
}
}
}
}
@ -238,7 +163,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
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 ->
downloadViewModel.deleteDownload(item)
downloadViewModel.deleteDownload(item.id)
}
deleteDialog.show()
}
@ -252,7 +177,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
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
}
@ -273,39 +198,53 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
downloadViewModel.deleteDownload(obj)
lifecycleScope.launch {
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()
true
}
R.id.redownload -> {
runBlocking {
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList())
lifecycleScope.launch {
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()
}
true
}
R.id.select_all -> {
erroredDownloads.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
true
}
R.id.invert_selected -> {
erroredDownloads.invertSelected(items)
val invertedList = arrayListOf<DownloadItem>()
items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
adapter.invertSelected()
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0) actionMode?.finish()
true
}
else -> false
@ -314,15 +253,10 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
clearCheckedItems()
adapter.clearCheckedItems()
}
}
private fun clearCheckedItems(){
erroredDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
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) {
val position = viewHolder.bindingAdapterPosition
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.RIGHT -> {
runBlocking{
downloadViewModel.queueDownloads(listOf(items[position]))
downloadViewModel.reQueueDownloadItems(listOf(itemID))
}
}
ItemTouchHelper.LEFT -> {
val deletedItem = items[position]
downloadViewModel.deleteDownload(deletedItem)
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()
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
downloadViewModel.deleteDownload(deletedItem.id)
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(
c,
recyclerView,
viewHolder!!,
viewHolder,
dX,
dY,
actionState,

View file

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

View file

@ -6,23 +6,15 @@ import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
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.repository.DownloadRepository
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.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
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 java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.UUID
import kotlinx.coroutines.withContext
class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -63,12 +52,11 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : GenericDownloadAdapter
private lateinit var adapter : GenericDownloadAdapter
private lateinit var notificationUtil: NotificationUtil
private lateinit var fileSize: TextView
private var selectedObjects: ArrayList<DownloadItem>? = null
private var totalSize: Int = 0
private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem>
override fun onCreateView(
inflater: LayoutInflater,
@ -79,8 +67,6 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
activity = getActivity()
notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
selectedObjects = arrayListOf()
return fragmentView
}
@ -88,7 +74,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fileSize = view.findViewById(R.id.filesize)
queuedDownloads =
adapter =
GenericDownloadAdapter(
this,
requireActivity()
@ -96,7 +82,9 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
queuedRecyclerView = view.findViewById(R.id.download_recyclerview)
queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = queuedDownloads
queuedRecyclerView.adapter = adapter
queuedRecyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -104,18 +92,34 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
queuedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.queuedDownloads.observe(viewLifecycleOwner) {
items = it.toMutableList()
if (it.isEmpty()) fileSize.visibility = View.GONE
else{
val size = FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize })
if (size == "?") fileSize.visibility = View.GONE
else {
fileSize.visibility = View.VISIBLE
fileSize.text = "${getString(R.string.file_size)}: ~ $size"
lifecycleScope.launch {
downloadViewModel.queuedDownloads.collectLatest {
adapter.submitData(it)
}
}
adapter.addLoadStateListener { loadState ->
lifecycleScope.launch {
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) {
val item = items.find { it.id == itemID } ?: return
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)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
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)
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)
item.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeItem(it.id)
},
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
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) {
val item = items.find { it.id == itemID }
val now = System.currentTimeMillis()
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
override fun onCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
val now = System.currentTimeMillis()
if (isChecked) {
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
}
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
actionMode?.finish()
else {
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0){
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){
val item = items.find { it.id == id } ?: return
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)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
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)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
}
deleteDialog.show()
}
deleteDialog.show()
}
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
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
}
@ -318,46 +246,81 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
val id = obj.id.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id)
downloadViewModel.deleteDownload(obj)
lifecycleScope.launch {
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()
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()
true
}
R.id.download -> {
for (obj in selectedObjects!!){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(obj.id.toInt().toString())
}
selectedObjects!!.forEach { it.downloadStartTime = 0L }
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.queueDownloads(selectedObjects!!)
lifecycleScope.launch {
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()
for (id in selectedObjects){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
}
withContext(Dispatchers.IO) {
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
}
actionMode?.finish()
}
true
}
R.id.select_all -> {
queuedDownloads.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
true
}
R.id.invert_selected -> {
queuedDownloads.invertSelected(items)
val invertedList = arrayListOf<DownloadItem>()
items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
adapter.invertSelected()
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
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
}
else -> false
@ -366,14 +329,10 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
clearCheckedItems()
adapter.clearCheckedItems()
}
}
private fun clearCheckedItems(){
queuedDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
@ -383,12 +342,16 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
val deletedItem = items[position]
queuedDownloads.notifyItemChanged(position)
removeItem(deletedItem.id)
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeItem(deletedItem.id)
}
}
}
@ -426,7 +389,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
super.onChildDraw(
c,
recyclerView,
viewHolder!!,
viewHolder,
dX,
dY,
actionState,

View file

@ -1,27 +1,21 @@
package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
@ -29,19 +23,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
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.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
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.withContext
class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -49,11 +46,9 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var savedRecyclerView: RecyclerView
private lateinit var savedDownloads: GenericDownloadAdapter
private var selectedObjects: ArrayList<DownloadItem>? = null
private lateinit var adapter: GenericDownloadAdapter
private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem>
private var totalSize : Int = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -61,16 +56,14 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
activity = getActivity()
selectedObjects = arrayListOf()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
savedDownloads =
adapter =
GenericDownloadAdapter(
this,
requireActivity()
@ -78,7 +71,8 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
savedRecyclerView = view.findViewById(R.id.download_recyclerview)
savedRecyclerView.forceFastScrollMode()
savedRecyclerView.adapter = savedDownloads
savedRecyclerView.enableFastScroll()
savedRecyclerView.adapter = adapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
@ -87,148 +81,76 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
savedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
downloadViewModel.savedDownloads.observe(viewLifecycleOwner) {
items = it.toMutableList()
savedDownloads.submitList(it)
lifecycleScope.launch {
downloadViewModel.savedDownloads.collectLatest {
adapter.submitData(it)
}
}
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Saved)).observe(viewLifecycleOwner){
totalSize = it
}
}
override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID }
if (item != null){
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
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) {
val item = items.find { it.id == itemID } ?: return
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)
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
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) {
val item = items.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
override fun onCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (isChecked) {
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
}else{
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
}
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
actionMode?.finish()
else {
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0){
actionMode?.finish()
}
}
}
}
@ -240,7 +162,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
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 ->
downloadViewModel.deleteDownload(item)
downloadViewModel.deleteDownload(item.id)
}
deleteDialog.show()
}
@ -249,7 +171,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
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
}
@ -270,39 +192,54 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
downloadViewModel.deleteDownload(obj)
lifecycleScope.launch {
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()
true
}
R.id.download -> {
runBlocking {
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList())
lifecycleScope.launch {
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()
}
true
}
R.id.select_all -> {
savedDownloads.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
true
}
R.id.invert_selected -> {
savedDownloads.invertSelected(items)
val invertedList = arrayListOf<DownloadItem>()
items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
adapter.invertSelected()
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0) actionMode?.finish()
true
}
else -> false
@ -311,14 +248,10 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
clearCheckedItems()
adapter.clearCheckedItems()
}
}
private fun clearCheckedItems(){
savedDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
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) {
val position = viewHolder.bindingAdapterPosition
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
val deletedItem = items[position]
downloadViewModel.deleteDownload(deletedItem)
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()
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
downloadViewModel.deleteDownload(deletedItem.id)
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 -> {
onActionButtonClick(items[position].id)
onActionButtonClick(itemID)
}
}
}
@ -377,7 +314,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
super.onChildDraw(
c,
recyclerView,
viewHolder!!,
viewHolder,
dX,
dY,
actionState,

View file

@ -29,6 +29,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.CookieAdapter
import com.deniscerri.ytdlnis.database.models.CookieItem
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.chip.Chip
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.os.Bundle
import android.util.Log
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
@ -68,6 +69,7 @@ class WebViewActivity : BaseActivity() {
generateBtn.setOnClickListener {
cookiesViewModel.getCookiesFromDB().getOrNull()?.let {
Log.e("asas", it)
kotlin.runCatching {
lifecycleScope.launch {
withContext(Dispatchers.IO){

View file

@ -31,28 +31,6 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
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!!.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?.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!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
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()
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)}"
}else{
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")
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 =
Preference.OnPreferenceClickListener {
@ -314,6 +314,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
}
val arr = JsonArray()
items.forEach {
it.useAsExtraCommand = false
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr

View file

@ -10,6 +10,7 @@ import android.os.storage.StorageVolume
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
@ -20,6 +21,7 @@ import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.getBasePath
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -27,11 +29,13 @@ import okhttp3.internal.closeQuietly
import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
import kotlin.io.path.absolutePathString
import kotlin.math.log10
import kotlin.math.pow
@ -87,7 +91,7 @@ object FileUtil {
if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach
var destFile: DocumentFile
try {
if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)".toRegex())){
if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)|(live_chat)".toRegex())){
it.delete()
return@forEach
}
@ -182,8 +186,15 @@ object FileUtil {
super.onFailed(errorCode)
}
override fun onConflict(
destinationFile: DocumentFile,
action: FileConflictAction
) {
action.confirmResolution(ConflictResolution.CREATE_NEW)
}
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) {
@ -204,17 +215,37 @@ object FileUtil {
val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it)
for (ff in files){
val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}")
newFile.mkdirs()
if (Build.VERSION.SDK_INT >= 26 ) {
Files.move(
ff.toPath(),
newFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}else{
ff.renameTo(newFile)
runCatching {
newFile.parentFile?.mkdirs()
}
if (Build.VERSION.SDK_INT >= 26 ) {
var newFileName = newFile.absolutePath
var counter = 1
while (Files.exists(File(newFileName).toPath())) {
// 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++
}
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
}
@ -238,17 +269,25 @@ object FileUtil {
return listOf(context.getString(R.string.unfound_file))
}
fun getCachePath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/.CACHE"
}
fun 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{
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 {
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{

View file

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

View file

@ -75,6 +75,25 @@ class NotificationUtil(var context: Context) {
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(
pendingIntent: PendingIntent?,
@ -188,6 +207,7 @@ class NotificationUtil(var context: Context) {
R.drawable.ic_launcher_foreground_large
)
)
.setContentText("")
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
@ -277,6 +297,11 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
}
fun notify(id: Int, notification: Notification){
notificationManager.notify(id, notification)
}
fun updateDownloadNotification(
id: Int,
desc: String,
@ -344,6 +369,19 @@ class NotificationUtil(var context: Context) {
fun createYTDLUpdateNotification() : Notification{
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
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()
}

View file

@ -7,9 +7,12 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.net.Uri
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
@ -28,18 +31,26 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
@ -53,8 +64,11 @@ import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
object UiUtil {
@SuppressLint("SetTextI18n")
@ -280,7 +294,7 @@ object UiUtil {
if (! file.exists()) return@forEach
val uri = FileProvider.getUriForFile(
fragmentContext,
"com.deniscerri.ytdl.fileprovider",
fragmentContext.packageName + ".fileprovider",
file
)
uris.add(uri)
@ -336,6 +350,150 @@ object UiUtil {
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){
val bottomSheet = BottomSheetDialog(activity)
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")
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){
val scale = context.resources.displayMetrics.density
this.setText(t)
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
this.measure(widthMeasureSpec, heightMeasureSpec)
val requiredWidth: Int = this.measuredWidth
if (t.isEmpty()){
this.layoutParams.width = 70
if (t.length < 5){
this.layoutParams.width = (70 * scale + 0.5F).toInt()
}else{
this.layoutParams.width = requiredWidth
}

View file

@ -3,27 +3,15 @@ package com.deniscerri.ytdlnis.util
import android.content.Context
import androidx.media3.common.AudioAttributes
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.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.deniscerri.ytdlnis.App
import org.chromium.net.CronetEngine
import java.util.concurrent.Executors
object VideoPlayerUtil {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun buildPlayer(context: Context) : 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 loadControl = DefaultLoadControl.Builder()
@ -37,15 +25,8 @@ object VideoPlayerUtil {
)
.build()
val cronetDataSourceFactory = CronetDataSource.Factory(
cronetEngine,
Executors.newCachedThreadPool()
)
val dataSourceFactory = DefaultDataSource.Factory(context, cronetDataSourceFactory)
player = ExoPlayer.Builder(context)
.setUsePlatformDiagnostics(false)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true)

View file

@ -7,10 +7,12 @@ import android.util.Log
import android.widget.Toast
import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager
import androidx.work.Data
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.R
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.repository.DownloadRepository
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.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.random.Random
class DownloadWorker(
private val context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
override fun doWork(): Result {
itemId = inputData.getLong("id", 0)
if (itemId == 0L || isStopped) return Result.success()
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
if (isStopped) return Result.success()
val notificationUtil = NotificationUtil(context)
val infoUtil = InfoUtil(context)
val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao
val repository = DownloadRepository(dao)
val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao)
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?
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()
runningYTDLInstances.addAll(dao.getActiveDownloadsList().map { it.id })
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.createPendingIntent()
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
val foregroundInfo = ForegroundInfo(downloadItem.id.toInt(), notification)
var notification = notificationUtil.createDefaultWorkerNotification()
val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), notification)
setForegroundAsync(foregroundInfo)
Log.e(TAG, downloadItem.toString())
runBlocking{
YoutubeDL.getInstance().destroyProcessById(itemId.toString())
repository.setDownloadStatus(downloadItem, DownloadRepository.Status.Active)
}
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
CoroutineScope(Dispatchers.IO).launch {
//update item if its incomplete
updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
}
val request = infoUtil.buildYoutubeDLRequest(downloadItem)
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){
queuedItems.collectLatest { items ->
val running = ArrayList(runningYTDLInstances)
if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running }
eligibleDownloads.forEach {downloadItem ->
runningYTDLInstances.add(downloadItem.id)
CoroutineScope(Dispatchers.IO).launch {
withContext(Dispatchers.Main){
notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
notificationUtil.notify(downloadItem.id.toInt(), notification)
}
val request = infoUtil.buildYoutubeDLRequest(downloadItem)
downloadItem.status = DownloadRepository.Status.Active.toString()
CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id)
}
}
}
}.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))
}
//update item if its incomplete
updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
}
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)
}
val cacheDir = FileUtil.getCachePath()
val tempFileDir = File(cacheDir, downloadItem.id.toString())
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" +
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
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))
if (logDownloads){
runBlocking {
infoUtil.getFromYTDL(downloadItem.url).forEach { res ->
if (res != null) {
resultDao.insert(res)
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 {
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 {
if (it is YoutubeDL.CanceledException) {
return Result.success(
Data.Builder().putString("output", "Download has been cancelled!").build()
)
}else{
if (logDownloads){
CoroutineScope(Dispatchers.IO).launch {
if(it.message != null){
logRepo.update(it.message!!, logItem.id)
//put download in history
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 {
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()
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
)
return Result.success(
Data.Builder().putString("output", it.toString()).build()
)
}
if (eligibleDownloads.isNotEmpty()){
eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() }
dao.updateMultiple(eligibleDownloads)
}
}
return Result.success()
}
@ -242,7 +265,7 @@ class DownloadWorker(
var wasQuickDownloaded = false
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
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))
val info = infoUtil.getMissingInfo(downloadItem.url)
if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString()
@ -259,17 +282,10 @@ class DownloadWorker(
return wasQuickDownloaded
}
override fun onStopped() {
destroyYTDLProcess()
super.onStopped()
}
private fun destroyYTDLProcess(){
YoutubeDL.getInstance().destroyProcessById(itemId.toInt().toString())
}
companion object {
var itemId: Long = 0
val runningYTDLInstances: MutableList<Long> = mutableListOf()
const val TAG = "DownloadWorker"
}

View file

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

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:layout_height="wrap_content"
android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="3"

View file

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

View file

@ -156,11 +156,6 @@
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
android:clipToPadding="false"
android:paddingBottom="100dp"
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.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>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

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

View file

@ -92,11 +92,6 @@
android:paddingTop="10dp"
android:layout_height="wrap_content"
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"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

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

View file

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

View file

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

View file

@ -110,131 +110,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
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>
<include layout="@layout/adjust_audio" />
</LinearLayout>

View file

@ -69,50 +69,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<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>
<include layout="@layout/adjust_command" />
</LinearLayout>

View file

@ -110,209 +110,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<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>
<include layout="@layout/adjust_video" />
</LinearLayout>

View file

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

View file

@ -133,14 +133,8 @@
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
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"
/>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
@ -23,14 +24,7 @@
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:fastScrollEnabled="true"
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_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
@ -38,5 +32,5 @@
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

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

View file

@ -39,22 +39,6 @@
app:layout_constraintTop_toTopOf="@+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
android:orientation="vertical"
@ -72,13 +56,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginVertical="10dp">
android:layout_margin="10dp">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="10dp"
android:textStyle="bold"
android:layout_marginHorizontal="10dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
@ -93,14 +77,14 @@
<TextView
android:id="@+id/bottom_info"
android:layout_width="0dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="10dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
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_toTopOf="@+id/nestedScrollView"
app:layout_constraintEnd_toStartOf="@+id/download_music"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
@ -136,6 +120,7 @@
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
@ -150,12 +135,12 @@
<TextView
android:id="@+id/running"
android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="10dp"
android:layout_marginHorizontal="10dp"
android:text="@string/running"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -170,12 +155,12 @@
<TextView
android:id="@+id/queued"
android:layout_marginHorizontal="20dp"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="10dp"
android:layout_marginHorizontal="10dp"
android:text="@string/download_queue"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/running_recycler" />
@ -189,13 +174,11 @@
app:layout_constraintTop_toBottomOf="@id/queued" />
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="10dp"
android:singleLine="true"
android:text="@string/app_name"

View file

@ -23,12 +23,37 @@
app:menu="@menu/select_playlist_menu"
android:layout_gravity="bottom">
<TextView
android:id="@+id/selected"
android:layout_width="wrap_content"
android:textStyle="bold"
android:layout_marginHorizontal="30dp"
android:layout_height="wrap_content" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="70dp"
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>
@ -36,12 +61,10 @@
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/check_all"
android:layout_width="wrap_content"
android:text="@string/select_all"
app:icon="@drawable/ic_select_all"
android:layout_height="wrap_content"
app:layout_anchor="@id/bottomAppBar"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -56,72 +79,42 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
android:orientation="horizontal">
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/select"
android:textSize="20sp"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_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_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
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="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">
<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>
@ -136,11 +129,6 @@
android:focusableInTouchMode="true"
android:orientation="vertical"
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" />

View file

@ -16,12 +16,6 @@
android:icon="@drawable/ic_format"
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
android:id="@+id/folder"
android:title="@string/directories"
@ -29,5 +23,12 @@
app:showAsAction="always"/>
<item
android:id="@+id/more"
android:title="@string/more"
android:icon="@drawable/ic_more"
app:showAsAction="always"/>
</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"
app:showAsAction="ifRoom" />
<item
android:id="@+id/up"
android:icon="@drawable/ic_arrow_up"
app:showAsAction="ifRoom"
android:title="Move Up" />
<item
android:id="@+id/download"
android:title="@string/download"
android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/select_all"
android:title="@string/select_all"

View file

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

View file

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

View file

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

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:name="com.deniscerri.ytdlnis.ui.HomeFragment"
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
android:id="@+id/historyFragment"
android:name="com.deniscerri.ytdlnis.ui.downloads.HistoryFragment"
@ -80,4 +84,8 @@
android:id="@+id/downloadLogFragment"
android:name="com.deniscerri.ytdlnis.ui.more.downloadLogs.DownloadLogFragment"
android:label="DownloadLogFragment" />
<fragment
android:id="@+id/resultCardDetailsDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
android:label="ResultCardDetailsDialog" />
</navigation>

View file

@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="audio_containers">
<item>@string/defaultValue</item>
<item>mp3</item>
@ -65,12 +65,14 @@
<string-array name="format_ordering">
<item>@string/file_size</item>
<item>@string/container</item>
<item>@string/codec</item>
<item>ID</item>
</string-array>
<string-array name="format_ordering_values">
<item>filesize</item>
<item>container</item>
<item>codec</item>
<item>id</item>
</string-array>
@ -770,4 +772,22 @@
<item>queue</item>
</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>

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