major stuff pre 1.6.2

This commit is contained in:
zaednasr 2024-02-04 14:42:43 +01:00
parent e2ad3569f1
commit 8d9d06ab13
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
105 changed files with 4285 additions and 863 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "5cea521e1a26473b2ec5298791f10ac4",
"identityHash": "5be3b7ae26c7716f2192470a3a9249a8",
"entities": [
{
"tableName": "results",
@ -587,12 +587,131 @@
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "observeSources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `status` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyWeekDay` TEXT NOT NULL, `everyMonthDay` INTEGER NOT NULL, `everyTime` INTEGER NOT NULL, `startsTime` INTEGER NOT NULL, `startsMonth` TEXT NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `alreadyProcessedLinks` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadItemTemplate",
"columnName": "downloadItemTemplate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyNr",
"columnName": "everyNr",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "everyCategory",
"columnName": "everyCategory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyWeekDay",
"columnName": "everyWeekDay",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyMonthDay",
"columnName": "everyMonthDay",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "everyTime",
"columnName": "everyTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startsTime",
"columnName": "startsTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startsMonth",
"columnName": "startsMonth",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "endsDate",
"columnName": "endsDate",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "endsAfterCount",
"columnName": "endsAfterCount",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "runCount",
"columnName": "runCount",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "retryMissingDownloads",
"columnName": "retryMissingDownloads",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alreadyProcessedLinks",
"columnName": "alreadyProcessedLinks",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5cea521e1a26473b2ec5298791f10ac4')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5be3b7ae26c7716f2192470a3a9249a8')"
]
}
}

View file

@ -202,8 +202,9 @@
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelWorkReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.AlarmStartReceiver" />
<receiver android:name=".receiver.AlarmStopReceiver" />
<receiver android:name=".receiver.downloadAlarmReceivers.AlarmStartReceiver" />
<receiver android:name=".receiver.downloadAlarmReceivers.AlarmStopReceiver" />
<receiver android:name=".receiver.ObserveAlarmReceiver" />
<activity
android:name=".receiver.ResumeActivity"

View file

@ -163,7 +163,7 @@ class MainActivity : BaseActivity() {
if (navigationView is NavigationView){
(navigationView as NavigationView).setupWithNavController(navController)
//terminate button
(navigationView as NavigationView).menu.getItem(7).setOnMenuItemClickListener {
(navigationView as NavigationView).menu.getItem(8).setOnMenuItemClickListener {
if (preferences.getBoolean("ask_terminate_app", true)){
var doNotShowAgain = false
val terminateDialog = MaterialAlertDialogBuilder(this)
@ -204,7 +204,7 @@ class MainActivity : BaseActivity() {
true
}
//settings button
(navigationView as NavigationView).menu.getItem(8).setOnMenuItemClickListener {
(navigationView as NavigationView).menu.getItem(9).setOnMenuItemClickListener {
val intent = Intent(context, SettingsActivity::class.java)
startActivity(intent)
true
@ -244,7 +244,7 @@ class MainActivity : BaseActivity() {
if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){
if (UpdateUtil(this@MainActivity).updateYoutubeDL() == YoutubeDL.UpdateStatus.DONE) {
val version = YoutubeDL.getInstance().version(context)
Snackbar.make(findViewById(android.R.id.content),
Snackbar.make(findViewById(R.id.frame_layout),
this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]",
Snackbar.LENGTH_LONG).show()
}
@ -254,6 +254,8 @@ class MainActivity : BaseActivity() {
}
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.database
import androidx.room.TypeConverter
import com.deniscerri.ytdlnis.database.models.AudioPreferences
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@ -57,4 +58,19 @@ class Converters {
val gson = Gson()
return gson.toJson(list)
}
@TypeConverter
fun downloadItemToString(downloadItem: DownloadItem) : String {
val gson = Gson()
return gson.toJson(downloadItem)
}
@TypeConverter
fun stringToDownloadItem(s: String) = Gson().fromJson(s, DownloadItem::class.java)
@TypeConverter
fun mutableListOfStringsToString(s: MutableList<String>) = Gson().toJson(s)
@TypeConverter
fun stringtoMutableListofStrings(s: String) = Gson().fromJson(s, Array<String>::class.java).toMutableList()
}

View file

@ -21,7 +21,8 @@ import com.deniscerri.ytdlnis.database.models.*
TemplateShortcut::class,
CookieItem::class,
LogItem::class,
TerminalItem::class
TerminalItem::class,
ObserveSourcesItem::class
],
version = 14,
autoMigrations = [
@ -37,7 +38,7 @@ import com.deniscerri.ytdlnis.database.models.*
AutoMigration (from = 10, to = 11),
AutoMigration (from = 11, to = 12),
AutoMigration (from = 12, to = 13),
//AutoMigration (from = 13, to = 14), MANUALLY HANDLED
// AutoMigration (from = 13, to = 14) MANUALLY HANDLED
]
)
abstract class DBManager : RoomDatabase(){
@ -49,6 +50,7 @@ abstract class DBManager : RoomDatabase(){
abstract val cookieDao: CookieDao
abstract val logDao: LogDao
abstract val terminalDao: TerminalDao
abstract val observeSourcesDao: ObserveSourcesDao
enum class SORTING{
DESC, ASC

View file

@ -1,39 +1,7 @@
package com.deniscerri.ytdlnis.database
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.graphics.Outline
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import android.util.DisplayMetrics
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.Px
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView
import androidx.room.migration.Migration
import androidx.work.impl.Migration_12_13
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File
import java.util.regex.Pattern
object Migrations {
@ -48,7 +16,10 @@ object Migrations {
val path = cursor.getString(cursor.getColumnIndex("downloadPath"))
val newPath = "[\"${path.replace("\"", "\\\"").replace("'", "''")}\"]"
database.execSQL("UPDATE history SET downloadPath = '${newPath}' WHERE id = $id")
}
database.execSQL("CREATE TABLE IF NOT EXISTS `observeSources` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `status` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyWeekDay` TEXT NOT NULL, `everyMonthDay` INTEGER NOT NULL, `everyTime` INTEGER NOT NULL, `startsTime` INTEGER NOT NULL, `startsMonth` TEXT NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `alreadyProcessedLinks` TEXT NOT NULL)")
}
)

View file

@ -9,6 +9,9 @@ interface CookieDao {
@Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookies() : List<CookieItem>
@Query("SELECT * FROM cookies WHERE url = :url LIMIT 1")
fun getByURL(url: String) : CookieItem
@Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookiesFlow() : Flow<List<CookieItem>>

View file

@ -16,6 +16,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')")
fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloads() : Flow<List<DownloadItem>>
@Query("SELECT COUNT(*) FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')")
fun getActiveDownloadsCountFlow() : Flow<Int>
@ -25,9 +28,29 @@ interface DownloadDao {
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatus(status : List<String>) : Int
@Query("""
SELECT COUNT(*) FROM downloads WHERE status = 'Processing'
UNION
SELECT COUNT(*) FROM downloads WHERE status ='Processing' AND type =
(SELECT type from downloads WHERE status = 'Processing' ORDER BY id LIMIT 1)
""")
fun getProcessingDownloadsCountByType() : List<Int>
@Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)")
suspend fun updateItemsToProcessing(ids: List<Long>)
@Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem
@Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused','PausedReQueued')")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem>
@ -126,6 +149,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Saved'")
suspend fun deleteSaved()
@Query("DELETE FROM downloads WHERE status='Processing'")
suspend fun deleteProcessing()
@Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>)
@ -174,12 +200,18 @@ interface DownloadDao {
@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("Select url from downloads where status in (:status)")
fun getURLsByStatus(status: List<String>) : List<String>
@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>)
@Query("Update downloads SET status='Saved' WHERE status='Processing'")
fun updateProcessingtoSavedStatus()
@Transaction
fun putAtTopOfTheQueue(id: Long){
val downloads = getQueuedDownloadsList()

View file

@ -0,0 +1,36 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import kotlinx.coroutines.flow.Flow
@Dao
interface ObserveSourcesDao {
@Query("SELECT * FROM observeSources ORDER BY id DESC")
fun getAllSources() : List<ObserveSourcesItem>
@Query("SELECT * FROM observeSources WHERE url = :url LIMIT 1")
fun getByURL(url: String) : ObserveSourcesItem
@Query("SELECT * FROM observeSources WHERE id = :id LIMIT 1")
fun getByID(id: Long) : ObserveSourcesItem
@Query("SELECT * FROM observeSources ORDER BY id DESC")
fun getAllSourcesFlow() : Flow<List<ObserveSourcesItem>>
@Query("SELECT EXISTS(SELECT * FROM observeSources WHERE url=:url LIMIT 1)")
fun checkIfExistsWithSameURL(url: String) : Boolean
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: ObserveSourcesItem) : Long
@Query("DELETE FROM observeSources")
suspend fun deleteAll()
@Query("DELETE FROM observeSources WHERE id=:itemId")
suspend fun delete(itemId: Long)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ObserveSourcesItem)
}

View file

@ -27,5 +27,7 @@ data class Format(
@SerializedName(value = "url")
var url: String? = "",
@SerializedName(value = "language", alternate = ["audioTrackLocale"])
val lang: String? = ""
val lang: String? = "",
@SerializedName(value = "tbr", alternate = ["bitrate"])
var tbr: String? = ""
) : Parcelable

View file

@ -0,0 +1,37 @@
package com.deniscerri.ytdlnis.database.models
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import kotlinx.parcelize.Parcelize
import java.time.Month
import java.util.Calendar
import java.util.Date
@Entity(tableName = "observeSources")
@Parcelize
data class ObserveSourcesItem(
@PrimaryKey(autoGenerate = true)
var id: Long,
var name: String,
var url: String,
var downloadItemTemplate: DownloadItem,
var status: ObserveSourcesRepository.SourceStatus,
var everyNr: Int,
var everyCategory: ObserveSourcesRepository.EveryCategory,
val everyWeekDay: List<String>,
val everyMonthDay: Int,
val everyTime: Long,
var startsTime: Long,
val startsMonth: Month,
@ColumnInfo(defaultValue = "0")
var endsDate: Long,
@ColumnInfo(defaultValue = "0")
var endsAfterCount: Int,
@ColumnInfo(defaultValue = "0")
var runCount: Int,
var retryMissingDownloads: Boolean,
var alreadyProcessedLinks : MutableList<String>
) : Parcelable

View file

@ -10,6 +10,7 @@ data class VideoPreferences (
var splitByChapters: Boolean = false,
var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false,
var writeAutoSubs: Boolean = false,
var subsLanguages: String = "en.*,.*-orig",
var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false

View file

@ -11,6 +11,10 @@ class CookieRepository(private val cookieDao: CookieDao) {
return cookieDao.getAllCookies()
}
fun getByURL(url: String) : CookieItem {
return cookieDao.getByURL(url)
}
suspend fun insert(item: CookieItem) : Long{
if (! cookieDao.checkIfExistsWithSameURL(item.url)){

View file

@ -18,6 +18,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getAllDownloads()}
)
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getQueuedDownloads()}
@ -38,7 +39,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCountFlow()
enum class Status {
Active, ActivePaused, PausedReQueued, Queued, QueuedPaused, Error, Cancelled, Saved
Active, ActivePaused, PausedReQueued, Queued, QueuedPaused, Error, Cancelled, Saved, Processing
}
suspend fun insert(item: DownloadItem) : Long {
@ -58,7 +59,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
private fun deleteCache(items: List<DownloadItem>) {
val cacheDir = FileUtil.getCachePath(App.instance)
items.forEach {
File(cacheDir, it.id.toString()).deleteRecursively()
runCatching { File(cacheDir, it.id.toString()).deleteRecursively() }
}
}
@ -84,6 +85,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndPausedDownloadsList()
}
fun getProcessingDownloads() : List<DownloadItem> {
return downloadDao.getProcessingDownloadsList()
}
fun getActiveAndQueuedDownloads() : List<DownloadItem> {
return downloadDao.getActiveAndQueuedDownloadsList()
}
@ -124,6 +129,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.deleteSaved()
}
suspend fun deleteProcessing(){
downloadDao.deleteProcessing()
}
suspend fun deleteAllWithIDs(ids: List<Long>){
downloadDao.deleteAllWithIDs(ids)

View file

@ -26,8 +26,8 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getAllHistoryByURL(url)
}
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) : List<HistoryItem> {
return when(sortType){
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List<HistoryItem> {
var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, format, site, sort.toString())
@ -39,6 +39,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
}
}
}
if(notDeleted){
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
}
return filtered
}
suspend fun insert(item: HistoryItem){

View file

@ -0,0 +1,60 @@
package com.deniscerri.ytdlnis.database.repository
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.dao.ObserveSourcesDao
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import kotlinx.coroutines.flow.Flow
class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao) {
val items : Flow<List<ObserveSourcesItem>> = observeSourcesDao.getAllSourcesFlow()
enum class SourceStatus {
ACTIVE, STOPPED
}
enum class EveryCategory {
DAY, WEEK, MONTH
}
companion object {
val everyCategoryName = mapOf(
EveryCategory.DAY to R.string.day,
EveryCategory.WEEK to R.string.week,
EveryCategory.MONTH to R.string.month
)
}
fun getAll() : List<ObserveSourcesItem> {
return observeSourcesDao.getAllSources()
}
fun getByURL(url: String) : ObserveSourcesItem {
return observeSourcesDao.getByURL(url)
}
fun getByID(id: Long) : ObserveSourcesItem {
return observeSourcesDao.getByID(id)
}
suspend fun insert(item: ObserveSourcesItem) : Long{
if (!observeSourcesDao.checkIfExistsWithSameURL(item.url)){
return observeSourcesDao.insert(item)
}
return -1
}
suspend fun delete(item: ObserveSourcesItem){
observeSourcesDao.delete(item.id)
}
suspend fun deleteAll(){
observeSourcesDao.deleteAll()
}
suspend fun update(item: ObserveSourcesItem){
observeSourcesDao.update(item)
}
}

View file

@ -65,6 +65,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
val items : ArrayList<ResultItem?> = arrayListOf()
do {
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0]?.playlistTitle ?: "" else "")
val ids = resultDao.insertMultiple(tmp.videos.toList())
ids.forEachIndexed { index, id ->
tmp.videos[index]?.id = id
}
items.addAll(tmp.videos)
val tmpToken = tmp.nextPageToken
if (tmpToken.isEmpty()) break
@ -72,10 +76,6 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
nextPageToken = tmpToken
} while (true)
itemCount.value = items.size
val ids = resultDao.insertMultiple(items.toList())
ids.forEachIndexed { index, id ->
items[index]?.id = id
}
return items
}

View file

@ -17,6 +17,7 @@ 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.CookiesFragment
import com.deniscerri.ytdlnis.ui.more.WebViewActivity
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.Dispatchers
@ -43,6 +44,10 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
return repository.getAll()
}
fun getByURL(url: String) : CookieItem {
return repository.getByURL(url)
}
suspend fun insert(item: CookieItem) : Long {
return repository.insert(item)
}

View file

@ -10,6 +10,7 @@ import android.net.ConnectivityManager
import android.os.Looper
import android.util.DisplayMetrics
import android.widget.Toast
import androidx.compose.animation.core.updateTransition
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
@ -17,6 +18,7 @@ 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.OneTimeWorkRequestBuilder
@ -39,10 +41,13 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.ui.downloadcard.FormatTuple
import com.deniscerri.ytdlnis.util.DownloadUtil
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -57,7 +62,7 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
class DownloadViewModel(application: Application) : AndroidViewModel(application) {
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager
val repository : DownloadRepository
private val sharedPreferences: SharedPreferences
@ -66,6 +71,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItem>>
val activeDownloadsCount : Flow<Int>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
@ -89,19 +95,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private val dao: DownloadDao
private val historyRepository: HistoryRepository
data class DownloadsUiState(
var errorMessage: Pair<Int, Int>?,
//action title, type, data
var actions: MutableList<Triple<Int, DownloadsAction, List<DownloadItem>?>>?
data class AlreadyExistsUIState(
var historyItems: MutableList<Long>,
var downloadItems : MutableList<Long>
)
enum class DownloadsAction {
DOWNLOAD_ANYWAY
}
val uiState: MutableStateFlow<DownloadsUiState> = MutableStateFlow(DownloadsUiState(
errorMessage = null,
actions = null
val alreadyExistsUiState: MutableStateFlow<AlreadyExistsUIState> = MutableStateFlow(AlreadyExistsUIState(
historyItems = mutableListOf(),
downloadItems = mutableListOf()
))
enum class Type {
@ -126,6 +127,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads
processingDownloads = repository.processingDownloads
activeDownloadsCount = repository.activeDownloadsCount
savedDownloads = repository.savedDownloads.flow
cancelledDownloads = repository.cancelledDownloads.flow
@ -223,6 +225,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
@ -261,6 +264,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
addChapters, false,
ArrayList(sponsorblock),
saveSubs,
saveAutoSubs,
audioFormatIDs = preferredAudioFormats
)
@ -391,6 +395,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
fun createDownloadItemFromHistory(historyItem: HistoryItem) : DownloadItem {
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
@ -424,7 +429,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs)
val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, saveAutoSubs)
var path = defaultPath
historyItem.downloadPath.first().apply {
File(this).parent?.apply {
@ -480,7 +485,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.audio -> {
return cloneFormat (
try {
val theFormats = formats.filter { it.format_note.contains("audio", ignoreCase = true) }
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
val requirements = getPreferredAudioRequirements()
theFormats.maxByOrNull { f -> requirements.count{req -> req(f)} } ?: throw Exception()
}catch (e: Exception){
@ -492,7 +497,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.video -> {
return cloneFormat(
try {
val theFormats = formats.filter { !it.format_note.contains("audio", true) }.ifEmpty { defaultVideoFormats.sortedByDescending { it.filesize } }
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "null" }.ifEmpty { defaultVideoFormats.sortedByDescending { it.filesize } }
when (videoQualityPreference) {
"worst" -> {
theFormats.last()
@ -587,6 +592,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
}
fun insertToProcessing(items: List<DownloadItem>)= viewModelScope.launch(Dispatchers.IO){
repository.deleteProcessing()
items.forEach{
it.status = DownloadRepository.Status.Processing.toString()
repository.insert(it)
}
}
fun deleteCancelled() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteCancelled()
}
@ -599,6 +612,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
repository.deleteSaved()
}
fun deleteProcessing() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
}
fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){
repository.deleteAllWithIDs(ids)
}
@ -646,27 +663,31 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items)
startDownloadWorker(emptyList())
DownloadUtil.startDownloadWorker(emptyList(), application)
}
suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{
dbManager.downloadDao.putAtTopOfTheQueue(id)
startDownloadWorker(emptyList())
DownloadUtil.startDownloadWorker(emptyList(), application)
}
suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.reQueueDownloadItems(items)
startDownloadWorker(emptyList())
DownloadUtil.startDownloadWorker(emptyList(), application)
}
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicate : Boolean = false) = CoroutineScope(Dispatchers.IO).launch {
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : Pair<List<Long>, List<Long>> {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val queuedItems = mutableListOf<DownloadItem>()
val existing = mutableListOf<DownloadItem>()
val existingDownloads = mutableListOf<Long>()
val existingHistory = mutableListOf<Long>()
var ignoreDuplicate = ign
if (sharedPreferences.getBoolean("download_archive", false)) ignoreDuplicate = true
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
@ -680,122 +701,85 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
items.forEach {
if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString()
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null && !ignoreDuplicate) {
it.id = existingDownload.id
existing.add(it)
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
var alreadyExists = false
if(!ignoreDuplicate){
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
val existingHistory = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistory != null && !ignoreDuplicate){
it.id = existingHistory.id
existing.add(it)
if (existingDownload != null){
it.status = DownloadRepository.Status.Saved.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingDownloads.add(id)
}else{
if (it.id == 0L){
val id = runBlocking {
repository.insert(it)
}
it.id = id
}else if (it.status == DownloadRepository.Status.Queued.toString()){
withContext(Dispatchers.IO){
repository.update(it)
}
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
queuedItems.add(it)
val existingHistoryItem = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistoryItem != null){
alreadyExists = true
existingHistory.add(existingHistoryItem.id)
}
}
}
if (!alreadyExists){
if (it.id == 0L){
val id = runBlocking {
repository.insert(it)
}
it.id = id
}else if (it.status == DownloadRepository.Status.Queued.toString()){
withContext(Dispatchers.IO){
repository.update(it)
}
}
queuedItems.add(it)
}
}
if (existingDownloads.isNotEmpty() || existingHistory.isNotEmpty()){
alreadyExistsUiState.update { u -> u.copy(existingHistory, existingDownloads) }
}
if (queuedItems.isNotEmpty()){
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime()){
DownloadUtil.startDownloadWorker(queuedItems, context)
if(!useScheduler){
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){}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
}catch (ignored: Exception){}
}
}
}
}
if (existing.isNotEmpty()){
uiState.update { u -> u.copy(
errorMessage = Pair(R.string.download_already_exists, R.string.download_already_exists_summary),
actions = mutableListOf(
Triple(R.string.download, DownloadsAction.DOWNLOAD_ANYWAY, existing),
)
)
}
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
startDownloadWorker(queuedItems)
if(!useScheduler){
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){}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
}catch (ignored: Exception){}
}
}
}
}
@SuppressLint("RestrictedApi")
private suspend fun startDownloadWorker(queuedItems: List<DownloadItem>) {
val context = App.instance
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context)
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 = 0L
if (queuedItems.isNotEmpty()){
delay = if (queuedItems[0].downloadStartTime != 0L){
queuedItems[0].downloadStartTime.minus(currentTime)
} else 0
if (delay <= 60000L) delay = 0L
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){
Looper.prepare().run {
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}
}
return Pair(existingDownloads, existingHistory)
}
fun getQueuedCollectedFileSize() : Long {
@ -814,6 +798,132 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() })
}
fun moveProcessingToSavedCategory(){
dao.updateProcessingtoSavedStatus()
}
suspend fun downloadProcessingDownloads(timeInMillis: Long = 0){
repository.getProcessingDownloads().apply {
if (timeInMillis > 0){
this.forEach {
it.downloadStartTime = timeInMillis
}
}
queueDownloads(this)
}
}
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
i.format = selectedFormats[index].format
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
updateDownload(i)
}
return items.map { it.format.filesize }
}
suspend fun updateProcessingCommandFormat(format: Format){
val items = repository.getProcessingDownloads()
items.forEach { i ->
i.format = format
updateDownload(i)
}
}
suspend fun updateProcessingDownloadPath(path: String){
val items = repository.getProcessingDownloads()
items.forEach { i ->
i.downloadPath = path
updateDownload(i)
}
}
fun getProcessingDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Processing.toString()))
}
fun getProcessingDownloads() : List<DownloadItem> {
return repository.getProcessingDownloads()
}
suspend fun updateProcessingAllFormats(formatCollection: List<List<Format>>) {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
}
i.format = getFormat(i.allFormats, i.type)
kotlin.runCatching {
dbManager.resultDao.getResultByURL(i.url)?.apply {
this.formats = formatCollection[index].toMutableList()
dbManager.resultDao.update(this)
}
}
updateDownload(i)
}
}
suspend fun continueUpdatingFormatsOnBackground(){
dao.getProcessingDownloadsList().apply {
this.forEach {
it.status = DownloadRepository.Status.Saved.toString()
updateDownload(it)
}
val ids = this.map { it.id }
val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()
.setInputData(
Data.Builder()
.putLongArray("ids", ids.toLongArray())
.putInt("id", id)
.build())
.addTag("updateFormats")
.build()
val context = App.instance
WorkManager.getInstance(context).enqueueUniqueWork(
id.toString(),
ExistingWorkPolicy.REPLACE,
workRequest
)
}
}
suspend fun updateProcessingType(newType: Type) {
repository.getProcessingDownloads().apply {
val new = switchDownloadType(this, newType)
new.forEach {
updateDownload(it)
}
}
}
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {
val counts = dao.getProcessingDownloadsCountByType()
val sameType = counts.size == 1 || counts[0] == counts[1]
val first = dao.getFirstProcessingDownload()
return Pair(sameType, first.type)
}
suspend fun updateItemsWithIdsToProcessingStatus(ids: List<Long>) : Type {
repository.deleteProcessing()
dao.updateItemsToProcessing(ids)
val first = dao.getFirstProcessingDownload()
return first.type
}
fun getURLsByStatus(list: List<DownloadRepository.Status>) : List<String> {
return dao.getURLsByStatus(list.map { it.toString() })
}
private fun updateDownloadItem(
downloadItem: DownloadItem,
infoUtil: InfoUtil,

View file

@ -22,6 +22,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
val websiteFilter = MutableLiveData("")
private val queryFilter = MutableLiveData("")
private val formatFilter = MutableLiveData("")
private val notDeletedFilter = MutableLiveData(false)
val allItems : LiveData<List<HistoryItem>>
private var _items = MediatorLiveData<List<HistoryItem>>()
@ -32,19 +33,23 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
allItems = repository.items.asLiveData()
_items.addSource(allItems){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!)
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
}
_items.addSource(formatFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!)
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
}
_items.addSource(sortType){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!)
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
}
_items.addSource(websiteFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!)
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
}
_items.addSource(queryFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!)
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
}
_items.addSource(notDeletedFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
}
}
@ -76,8 +81,12 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
formatFilter.value = filter
}
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort))
fun setNotDeleted(filter: Boolean){
notDeletedFilter.value = filter
}
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort, notDeleted))
}
fun getAll() : List<HistoryItem> {

View file

@ -0,0 +1,142 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver
import com.deniscerri.ytdlnis.util.DownloadUtil
import com.deniscerri.ytdlnis.util.Extensions.closestValue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.Month
import java.util.Calendar
class ObserveSourcesViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: ObserveSourcesRepository
val items: LiveData<List<ObserveSourcesItem>>
private val alarmManager : AlarmManager
private val preferences : SharedPreferences
init {
val dao = DBManager.getInstance(application).observeSourcesDao
repository = ObserveSourcesRepository(dao)
items = repository.items.asLiveData()
alarmManager = application.getSystemService(AlarmManager::class.java)
preferences = PreferenceManager.getDefaultSharedPreferences(application)
}
fun getAll(): List<ObserveSourcesItem> {
return repository.getAll()
}
fun getByURL(url: String) : ObserveSourcesItem {
return repository.getByURL(url)
}
fun getByID(id: Long) : ObserveSourcesItem {
return repository.getByID(id)
}
suspend fun insert(item: ObserveSourcesItem) : Long {
if (item.id > 0) {
repository.update(item)
observeTask(item)
return item.id
}
val id = repository.insert(item)
item.id = id
if (id > 0) observeTask(item)
return id
}
fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
suspend fun update(item: ObserveSourcesItem) {
repository.update(item)
}
private fun observeTask(it: ObserveSourcesItem){
val id = it.id
val c = Calendar.getInstance()
val date = Calendar.getInstance()
date.timeInMillis = it.startsTime
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = it.everyTime
c.set(Calendar.DAY_OF_MONTH, date.get(Calendar.DAY_OF_MONTH))
c.set(Calendar.MONTH, date.get(Calendar.MONTH))
c.set(Calendar.YEAR, date.get(Calendar.YEAR))
c.set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
c.set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
DownloadUtil.cancelObservationTaskByID(application, id)
val intent = Intent(application, ObserveAlarmReceiver::class.java)
intent.putExtra("id", id)
if (it.everyNr == 0) it.everyNr = 1
when(it.everyCategory){
ObserveSourcesRepository.EveryCategory.DAY -> {
alarmManager.setExact(
AlarmManager.RTC,
c.timeInMillis,
PendingIntent.getBroadcast(application, it.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
}
ObserveSourcesRepository.EveryCategory.WEEK -> {
if (it.everyWeekDay.isNotEmpty()){
val weekDayID = c.get(Calendar.DAY_OF_WEEK).toString()
val followingWeekDay = (it.everyWeekDay.firstOrNull { it.toInt() > weekDayID.toInt() } ?: it.everyWeekDay.minBy { it.toInt() }).toInt()
c.set(Calendar.DAY_OF_WEEK, followingWeekDay)
if(c.timeInMillis < System.currentTimeMillis()){
c.add(Calendar.DAY_OF_MONTH, 7)
}
}
alarmManager.setExact(
AlarmManager.RTC,
c.timeInMillis,
PendingIntent.getBroadcast(application, it.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
val theMonthIndex = Month.values().indexOf(it.startsMonth)
val currentMonthIndex = c.get(Calendar.MONTH)
if (theMonthIndex != currentMonthIndex){
c.set(Calendar.MONTH, theMonthIndex)
if (c.timeInMillis < System.currentTimeMillis()){
c.add(Calendar.YEAR, 1)
}
}
alarmManager.setExact(
AlarmManager.RTC,
c.timeInMillis,
PendingIntent.getBroadcast(application, it.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
}
}
}
}

View file

@ -105,7 +105,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
else -> res.addAll(repository.getDefault(inputQuery, resetResults))
}
} catch (e: Exception) {
if (e !is kotlinx.coroutines.CancellationException){
if (updateResultDataJob?.isCancelled == false){
uiState.update {it.copy(
processing = false,
errorMessage = Pair(R.string.no_results, e.message.toString()),

View file

@ -0,0 +1,38 @@
package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.ObserveSourceWorker
import java.util.concurrent.TimeUnit
class ObserveAlarmReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
p1?.apply {
val sourceID = p1.getLongExtra("id", 0)
if (sourceID == 0L) return
val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag(sourceID.toString())
.setConstraints(workConstraints.build())
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", sourceID).build())
WorkManager.getInstance(p0!!).enqueueUniqueWork(
sourceID.toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}
}

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.receiver
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@ -97,6 +98,7 @@ class ShareActivity : BaseActivity() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
navController = navHostFragment.findNavController()
navController.addOnDestinationChangedListener(object: NavController.OnDestinationChangedListener{
@SuppressLint("RestrictedApi")
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.receiver
package com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers
import android.content.BroadcastReceiver
import android.content.Context
@ -13,7 +13,6 @@ import java.util.concurrent.TimeUnit
class AlarmStartReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
Log.e("assa", "Start")
val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.receiver
package com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers
import android.content.BroadcastReceiver
import android.content.Context

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
@ -14,6 +15,7 @@ import android.util.Log
import android.util.Patterns
import android.view.*
import android.view.View.*
import android.view.animation.Interpolator
import android.widget.*
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
@ -35,14 +37,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem
import com.deniscerri.ytdlnis.database.models.SearchSuggestionType
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter
import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdlnis.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
@ -56,11 +59,11 @@ 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.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -76,13 +79,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var downloadSelectedFab: ExtendedFloatingActionButton? = null
private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: FloatingActionButton? = null
private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: CoordinatorLayout? = null
private var infoUtil: InfoUtil? = null
private var downloadQueue: ArrayList<ResultItem>? = null
private lateinit var resultViewModel : ResultViewModel
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var historyViewModel : HistoryViewModel
private var fragmentView: View? = null
private var activity: Activity? = null
@ -126,7 +130,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
uiHandler = Handler(Looper.getMainLooper())
selectedObjects = ArrayList()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadQueue = ArrayList()
resultsList = mutableListOf()
@ -167,7 +172,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchSuggestionsRecyclerView = view.findViewById(R.id.search_suggestions_recycler)
searchSuggestionsRecyclerView?.layoutManager = LinearLayoutManager(context)
searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter
searchSuggestionsRecyclerView?.enableFastScroll()
searchSuggestionsRecyclerView?.itemAnimator = null
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
@ -263,6 +268,30 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
}
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
withContext(Dispatchers.Main){
kotlin.runCatching {
UiUtil.handleExistingDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
res,
downloadViewModel,
historyViewModel)
}
}
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
mutableListOf(),
mutableListOf()
)
}
}
}
}
}
override fun onResume() {
@ -278,15 +307,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){
arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds")
val items = mutableListOf<DownloadItem>()
ids?.forEach {
items.add(downloadViewModel.getItemByID(it))
downloadViewModel.deleteDownload(it)
}
val ids = arguments?.getLongArray("downloadIds") ?: return@launch
val type = downloadViewModel.updateItemsWithIdsToProcessingStatus(ids.toList())
withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("downloads", items)
Pair("type", type)
))
}
}
@ -304,6 +329,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
clipboardFab?.isVisible = false
initSearch(searchView!!)
}
lifecycleScope.launch {
clipboardFab?.extend()
delay(1000)
clipboardFab?.shrink()
}
}
}
}
@ -679,11 +710,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
downloadViewModel.turnResultItemsToDownloadItems(resultsList!!)
}
if (sharedPreferences!!.getBoolean("download_card", true)) {
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("downloads", downloadList)
))
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(Pair("type", downloadList[0].type)))
} else {
downloadViewModel.queueDownloads(downloadList)
downloadList.chunked(100).forEach {
downloadViewModel.queueDownloads(it)
}
}
}
}
@ -741,8 +776,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
if (sharedPreferences!!.getBoolean("download_card", true)) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("downloads", downloadList)
Pair("type", downloadList[0].type)
))
} else {
downloadViewModel.queueDownloads(downloadList)
@ -827,8 +866,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
chip.text = text
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.setOnClickListener {
if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE
queriesChipGroup!!.removeView(chip)
if (queriesChipGroup!!.childCount == 0) queriesChipGroup!!.visibility = View.GONE
}
queriesChipGroup!!.addView(chip)
}
@ -849,7 +888,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
resultViewModel.removeSearchQueryFromHistory(text)
searchSuggestionsAdapter?.notifyItemRemoved(position)
updateSearchViewItems(searchView!!.editText.text)
}
deleteDialog.show()
}

View file

@ -22,7 +22,9 @@ import com.deniscerri.ytdlnis.R
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.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.dp
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -61,6 +63,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
card.tag = "${item!!.id}##card"
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.image_view)

View file

@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -56,6 +57,8 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.image_view)

View file

@ -0,0 +1,107 @@
package com.deniscerri.ytdlnis.ui.adapter
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
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
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val onItemClickListener: OnItemClickListener
private val activity: Activity
init {
this.onItemClickListener = onItemClickListener
this.activity = activity
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cardView: ConstraintLayout
init {
cardView = itemView as ConstraintLayout
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = LayoutInflater.from(parent.context)
.inflate(R.layout.already_exists_item, parent, false)
return ViewHolder(cardView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) ?: return
val alreadyExistsItem = holder.cardView
alreadyExistsItem.tag = item.first.id.toString()
alreadyExistsItem.popup()
val editBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_edit)
val deleteBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_delete)
val ttle = alreadyExistsItem.findViewById<Button>(R.id.already_exists_title)
ttle.text = item.first.title
CoroutineScope(Dispatchers.IO).launch {
}
ttle.setOnLongClickListener {
onItemClickListener.onDeleteItem(item.first, position, item.second)
true
}
editBtn.setOnClickListener {
onItemClickListener.onEditItem(item.first, position)
}
deleteBtn.setOnClickListener {
onItemClickListener.onDeleteItem(item.first, position, item.second)
}
ttle.setOnClickListener(null)
if (item.second != null){
ttle.setOnClickListener {
onItemClickListener.onShowHistoryItem(item.second!!)
}
}
}
interface OnItemClickListener {
fun onEditItem(downloadItem: DownloadItem, position: Int)
fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?)
fun onShowHistoryItem(id: Long)
}
companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Pair<DownloadItem, Long?>> = object : DiffUtil.ItemCallback<Pair<DownloadItem, Long?>>() {
override fun areItemsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean {
return oldItem.first.id == newItem.first.id
}
override fun areContentsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean {
return oldItem.first.id == newItem.first.id && oldItem.first.title == newItem.first.title && oldItem.first.author == newItem.first.author && oldItem.first.thumb == newItem.first.thumb
}
}
}
}

View file

@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.squareup.picasso.Picasso
@ -54,13 +55,16 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
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("home")){
val imageURL = item?.thumb
val imageURL = item.thumb
if (!imageURL.isNullOrBlank()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else {
@ -72,7 +76,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
}
val duration = card.findViewById<TextView>(R.id.duration)
duration.text = item!!.duration
duration.text = item.duration
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
@ -120,7 +124,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
if (btn.hasOnClickListeners()) btn.setOnClickListener(null)
btn.setOnClickListener {
onItemClickListener.onButtonClick(item.url)
onItemClickListener.onButtonClick(item.id)
}
when(item.type) {
@ -136,18 +140,18 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
}
card.setOnClickListener {
onItemClickListener.onCardClick(item.url)
onItemClickListener.onCardClick(item.id)
}
card.setOnLongClickListener {
onItemClickListener.onDelete(item.url); true
onItemClickListener.onDelete(item.id); true
}
}
interface OnItemClickListener {
fun onButtonClick(itemURL: String)
fun onCardClick(itemURL: String)
fun onDelete(itemURL: String)
fun onButtonClick(id: Long)
fun onCardClick(id: Long)
fun onDelete(id: Long)
}
companion object {
@ -157,7 +161,10 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
}
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.type == newItem.type && oldItem.format == newItem.format
return oldItem.title == newItem.title &&
oldItem.author == newItem.author &&
oldItem.type == newItem.type &&
oldItem.format == newItem.format
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView
class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CookieItem?, CookieAdapter.ViewHolder>(AsyncDifferConfig.Builder(
@ -41,6 +42,7 @@ class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.item
card.popup()
val title = card.findViewById<TextView>(R.id.title)
title.text = item?.url

View file

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -50,6 +51,7 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.item
card.popup()
val title = card.findViewById<TextView>(R.id.title)
title.text = item?.title

View file

@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -55,6 +56,8 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
if (item == null) return
card.tag = item.id.toString()

View file

@ -11,10 +11,8 @@ import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
@ -24,9 +22,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
@ -86,6 +82,8 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
@ -116,7 +114,7 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
author.text = item.author
val length = card.findViewById<TextView>(R.id.length)
length.text = item.duration
length.text = if(item.downloadPath.size == 1) item.duration else "[${item.downloadPath.size}]"
// TIME DOWNLOADED ----------------------------------

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.adapter
import android.animation.ValueAnimator
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.Color
@ -18,11 +19,14 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.squareup.picasso.Picasso
class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<ResultItem?, HomeAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
@ -55,6 +59,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val video = getItem(position)
val card = holder.cardView
card.popup()
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.result_image_view)

View file

@ -0,0 +1,187 @@
package com.deniscerri.ytdlnis.ui.adapter
import android.app.Activity
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
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.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.squareup.picasso.Picasso
import java.text.DateFormatSymbols
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<ObserveSourcesItem?, ObserveSourcesAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val onItemClickListener: OnItemClickListener
private val activity: Activity
init {
this.onItemClickListener = onItemClickListener
this.activity = activity
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cardView: MaterialCardView
init {
cardView = itemView.findViewById(R.id.observe_sources_card)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = LayoutInflater.from(parent.context)
.inflate(R.layout.observe_sources_item, parent, false)
return ViewHolder(cardView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
val uiHandler = Handler(Looper.getMainLooper())
card.popup()
if (item == null) return
card.tag = item.url
holder.itemView.tag = item.url
val thumbnail = card.findViewById<ImageView>(R.id.result_image_view)
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
var title = item.name
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}
itemTitle.text = title
//URL
val url = card.findViewById<TextView>(R.id.url)
url.text = item.url
//INFO
val info = card.findViewById<Chip>(R.id.info)
val c = Calendar.getInstance()
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = item.everyTime
c.set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
c.set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
when(item.everyCategory){
ObserveSourcesRepository.EveryCategory.DAY -> {
c.add(Calendar.DAY_OF_MONTH, item.everyNr)
}
ObserveSourcesRepository.EveryCategory.WEEK -> {
if(item.everyWeekDay.isEmpty()){
c.add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}else{
val weekDayID = c.get(Calendar.DAY_OF_WEEK).toString()
val followingWeekDay = (item.everyWeekDay.firstOrNull { it.toInt() > weekDayID.toInt() } ?: item.everyWeekDay.minBy { it.toInt() }).toInt()
c.set(Calendar.DAY_OF_WEEK, followingWeekDay)
if (item.everyNr > 1){
c.add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}
}
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
c.add(Calendar.MONTH, item.everyNr)
c.set(Calendar.DAY_OF_MONTH, item.everyMonthDay)
}
}
val weekdays = DateFormatSymbols(Locale.getDefault()).shortWeekdays
val text = "${weekdays[c.get(Calendar.DAY_OF_WEEK)]}, ${SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(c.timeInMillis)}"
info.text = text
//CHECK MISSING
val checkMissing = card.findViewById<Button>(R.id.check_missing)
checkMissing.isVisible = item.retryMissingDownloads
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.download_progress)
progressBar.isIndeterminate = true
progressBar.isVisible = false
// BUTTON ----------------------------------
val btn = card.findViewById<FloatingActionButton>(R.id.search)
btn.isEnabled = true
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){
info.isVisible = false
checkMissing.isVisible = false
btn.setImageResource(R.drawable.exomedia_ic_play_arrow_white)
btn.setOnClickListener {
btn.isEnabled = false
progressBar.isVisible = true
progressBar.animate()
onItemClickListener.onItemStart(item, position)
}
}else{
info.isVisible = true
checkMissing.isVisible = true
btn.setImageResource(R.drawable.ic_search)
btn.setOnClickListener {
btn.isEnabled = false
progressBar.isVisible = true
progressBar.animate()
onItemClickListener.onItemSearch(item)
}
}
card.setOnClickListener {
onItemClickListener.onItemClick(item)
}
card.setOnLongClickListener {
onItemClickListener.onDelete(item); true
}
}
interface OnItemClickListener {
fun onItemSearch(item: ObserveSourcesItem)
fun onItemStart(item: ObserveSourcesItem, position: Int)
fun onItemClick(item: ObserveSourcesItem)
fun onDelete(item: ObserveSourcesItem)
}
companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<ObserveSourcesItem> = object : DiffUtil.ItemCallback<ObserveSourcesItem>() {
override fun areItemsTheSame(oldItem: ObserveSourcesItem, newItem: ObserveSourcesItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ObserveSourcesItem, newItem: ObserveSourcesItem): Boolean {
return oldItem.id == newItem.id &&
oldItem.name == newItem.name &&
oldItem.downloadItemTemplate == newItem.downloadItemTemplate &&
oldItem.status == newItem.status &&
oldItem.retryMissingDownloads == newItem.retryMissingDownloads &&
oldItem.runCount == newItem.runCount
}
}
}
}

View file

@ -20,6 +20,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.squareup.picasso.Picasso
import java.util.*
@ -56,6 +57,7 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)

View file

@ -62,6 +62,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
}
textView.setOnLongClickListener { true }
val mb = linear.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setImageResource(R.drawable.ic_arrow_outward)
mb.setOnClickListener {
onItemClickListener.onSearchSuggestionAddToSearchBar(item.text)
}
@ -79,6 +80,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
}
val mb = linear.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setImageResource(R.drawable.ic_arrow_outward)
mb.setOnClickListener {
onItemClickListener.onSearchSuggestionAddToSearchBar(item.text)
}

View file

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView
class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CommandTemplate?, TemplatesAdapter.ViewHolder>(AsyncDifferConfig.Builder(
@ -46,6 +47,7 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.item
card.popup()
val title = card.findViewById<TextView>(R.id.title)
title.text = item?.title

View file

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
@ -47,6 +48,8 @@ class TerminalDownloadsAdapter(onItemClickListener: OnItemClickListener, activit
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
card.tag = "${item!!.id}##card"
// PROGRESS BAR ----------------------------------------------------

View file

@ -10,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.edit
import androidx.lifecycle.ViewModelProvider
@ -22,12 +23,14 @@ import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -40,7 +43,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
private lateinit var fragmentAdapter : DownloadFragmentAdapter
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var commandTemplateDao: CommandTemplateDao
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener
private lateinit var sharedPreferences : SharedPreferences
@ -49,7 +52,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
onDownloadItemUpdateListener = listener
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
}
@ -84,7 +87,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
var commandTemplateNr = 0
lifecycleScope.launch{
withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateDao.getTotalNumber()
commandTemplateNr = commandTemplateViewModel.getTotalNumber()
if(commandTemplateNr <= 0){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f
@ -137,7 +140,19 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
override fun onTabSelected(tab: TabLayout.Tab?) {
if (tab!!.position == 2 && commandTemplateNr == 0){
tabLayout.selectTab(tabLayout.getTabAt(1))
Toast.makeText(context, getString(R.string.add_template_first), Toast.LENGTH_SHORT).show()
val s = Snackbar.make(view, getString(R.string.add_template_first), Snackbar.LENGTH_LONG)
val snackbarView: View = s.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
s.setAction(R.string.new_template){
UiUtil.showCommandTemplateCreationOrUpdatingSheet(item = null, context = requireActivity(), lifeCycle = this@ConfigureDownloadBottomSheetDialog, commandTemplateViewModel = commandTemplateViewModel){
commandTemplateNr = 1
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 1f
tabLayout.selectTab(tabLayout.getTabAt(2))
}
}
s.show()
}else if (tab.position == 1 && isAudioOnly){
tabLayout.selectTab(tabLayout.getTabAt(0))
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
@ -191,15 +206,15 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
return when(selectedTabPosition){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem
f.downloadItem.apply { id = currentDownloadItem.id }
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.downloadItem
f.downloadItem.apply { id = currentDownloadItem.id }
}
else -> {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.downloadItem
f.downloadItem.apply { id = currentDownloadItem.id }
}
}
}
@ -209,21 +224,27 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1
)
result.title = prevDownloadItem.title
result.author = prevDownloadItem.author
currentDownloadItem.title = prevDownloadItem.title
currentDownloadItem.author = prevDownloadItem.author
fragmentAdapter.setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
when(viewPager2.currentItem){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(0).format)
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(0).format)
}
}
2 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
else -> {}
}

View file

@ -31,6 +31,7 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -203,7 +204,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
//poll video progress
lifecycleScope.launch {
videoProgress(player).collect { p ->
val currentTime = infoUtil.formatIntegerDuration(p, Locale.US)
val currentTime = p.toStringDuration(Locale.US)
durationText.text = "$currentTime / ${item.duration}"
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
if (toTextInput.editText!!.text.isNotBlank()){
@ -310,11 +311,11 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
val endValue = (endSeconds.toFloat() / endTimestamp) * 100
if (seconds == 0) {
fromTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(startTimestamp, Locale.US))
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else if (startValue > 100){
startTimestamp = 0
seconds = 0
fromTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(startTimestamp, Locale.US))
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
startValue = 0F
}
@ -350,14 +351,14 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
var endValue = (seconds.toFloat() / endTimestamp) * 100
if (endValue > 100F){
endTimestamp = timeSeconds
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US))
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
endValue = 100F
}
if (seconds == 0) {
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US))
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}else if (endValue <= rangeSlider.valueFrom.toInt()){
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US))
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}
toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString())
@ -402,8 +403,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
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)
val startTimestampString = startTimestamp.toStringDuration(Locale.US)
val endTimestampString = endTimestamp.toStringDuration(Locale.US)
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString)

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
@ -18,6 +19,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -48,7 +50,7 @@ import kotlinx.coroutines.withContext
import java.io.File
class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment(), GUISync {
class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "", private var nonSpecific: Boolean = false) : Fragment(), GUISync {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
@ -63,6 +65,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
lateinit var author : TextInputLayout
lateinit var preferences: SharedPreferences
lateinit var shownFields: List<String>
@SuppressLint("RestrictedApi")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -95,7 +98,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
try {
title = view.findViewById(R.id.title_textinput)
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
title.visibility = if (shownFields.contains("title") && !nonSpecific) View.VISIBLE else View.GONE
if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title)
title.endIconMode = END_ICON_NONE
@ -109,7 +112,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
})
author = view.findViewById(R.id.author_textinput)
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
author.visibility = if (shownFields.contains("author") && !nonSpecific) View.VISIBLE else View.GONE
if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author)
author.endIconMode = END_ICON_NONE
@ -185,13 +188,22 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}
}
}
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) })
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { it.vcodec.isBlank() || it.vcodec == "none"})
val containers = requireContext().resources.getStringArray(R.array.audio_containers)
var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
if (nonSpecific){
val param = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT,
1.0f
)
container.layoutParams = param
container.setPadding(0)
}
val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview)

View file

@ -191,7 +191,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
updateItem.visibility = View.GONE
(updateItem.parent as LinearLayout).visibility = View.GONE
}
//remove outdated player url of 1hr so it can refetch it in the cut player
@ -393,27 +393,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
lifecycleScope.launch{
downloadViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null) {
withContext(Dispatchers.Main){
kotlin.runCatching {
UiUtil.handleDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
res,
downloadViewModel,
historyViewModel)
}
}
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
errorMessage = null,
actions = null
)
}
}
}
lifecycleScope.launch {
resultViewModel.uiState.collectLatest { res ->
@ -436,7 +416,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
shimmerLoadingSubtitle.visibility = View.VISIBLE
shimmerLoading.startShimmer()
shimmerLoadingSubtitle.startShimmer()
updateItem.visibility = View.GONE
(updateItem.parent as LinearLayout).visibility = View.GONE
}else{
title.visibility = View.VISIBLE
subtitle.visibility = View.VISIBLE
@ -652,5 +632,13 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
}
override fun onDismiss(dialog: DialogInterface) {
lifecycleScope.launch {
resultViewModel.cancelUpdateItemData()
resultViewModel.cancelUpdateFormatsItemData()
}
super.onDismiss(dialog)
}
}

View file

@ -77,7 +77,15 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO){
if (currentDownloadItem != null && currentDownloadItem!!.type == DownloadViewModel.Type.command){
if (currentDownloadItem != null){
currentDownloadItem?.apply {
if (type != DownloadViewModel.Type.command){
type = DownloadViewModel.Type.command
}
format = downloadViewModel.getFormat(allFormats, DownloadViewModel.Type.command)
}
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java)
}else{

View file

@ -11,7 +11,8 @@ class DownloadFragmentAdapter (
fragmentManager : FragmentManager,
lifecycle : Lifecycle,
private var result: ResultItem,
private var downloadItem: DownloadItem?
private var downloadItem: DownloadItem?,
private var nonSpecific: Boolean = false
) : FragmentStateAdapter(fragmentManager, lifecycle) {
fun setResultItem(res: ResultItem){
@ -35,8 +36,8 @@ class DownloadFragmentAdapter (
override fun createFragment(position: Int): Fragment {
return when(position){
0 -> DownloadAudioFragment(result, downloadItem)
1 -> DownloadVideoFragment(result, downloadItem)
0 -> DownloadAudioFragment(result, downloadItem,"", nonSpecific)
1 -> DownloadVideoFragment(result, downloadItem,"", nonSpecific)
else -> DownloadCommandFragment(result, downloadItem)
}
}

View file

@ -9,8 +9,9 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.view.LayoutInflater
@ -34,23 +35,21 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
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.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -82,16 +81,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView
private lateinit var sharedPreferences: SharedPreferences
private lateinit var items: MutableList<DownloadItem>
private lateinit var type: DownloadViewModel.Type
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
infoUtil = InfoUtil(requireContext())
}
@ -102,20 +99,15 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null)
dialog.setContentView(view)
if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelableArrayList("downloads", DownloadItem::class.java)
}else{
arguments?.getParcelableArrayList<DownloadItem>("downloads")
}.apply {
arguments?.getSerializable("type").apply {
if (this == null){
dismiss()
return
}else{
items = this
type = this as DownloadViewModel.Type
}
}
if (items.isEmpty()) dismiss()
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
@ -138,42 +130,52 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
recyclerView.adapter = listAdapter
recyclerView.enableFastScroll()
view.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
setOnClickListener {
recyclerView.scrollToPosition(0)
}
}
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("multipledownloadcard")){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
listAdapter.submitList(items.toList())
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
filesize = view.findViewById<TextView>(R.id.filesize)
filesize = view.findViewById(R.id.filesize)
scheduleBtn.setOnClickListener{
UiUtil.showDatePicker(parentFragmentManager) {
UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false
download.isEnabled = false
items.forEach { item ->
item.downloadStartTime = it.timeInMillis
}
runBlocking {
downloadViewModel.queueDownloads(items)
val first = items.first()
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.downloadProcessingDownloads(cal.timeInMillis)
}
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
dismiss()
}
dismiss()
}
}
download!!.setOnClickListener {
scheduleBtn.isEnabled = false
download.isEnabled = false
runBlocking {
downloadViewModel.queueDownloads(items)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.downloadProcessingDownloads()
}
dismiss()
}
dismiss()
}
download.setOnLongClickListener {
@ -182,9 +184,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch{
items.map { it.status = DownloadRepository.Status.Saved.toString() }
withContext(Dispatchers.IO){
downloadViewModel.insertAll(items)
downloadViewModel.moveProcessingToSavedCategory()
}
dismiss()
}
@ -195,7 +196,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
when(items.first().type){
when(type){
DownloadViewModel.Type.audio -> {
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
}
@ -212,72 +213,28 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) {
items.forEachIndexed { index, i ->
i.format = selectedFormats[index].format
if (i.type == DownloadViewModel.Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingFormat(selectedFormats)
}
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch(Dispatchers.IO){
val formatCollection = mutableListOf<List<Format>>()
allFormats.forEach {f ->
formatCollection.add(f.mapTo(mutableListOf()) {it.copy()})
}
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
}
i.format = downloadViewModel.getFormat(i.allFormats, i.type)
kotlin.runCatching {
val resultItem = resultViewModel.getItemByURL(i.url)!!
resultItem.formats = formatCollection[index].toMutableList()
resultViewModel.update(resultItem)
}
}
withContext(Dispatchers.Main){
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
}
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingAllFormats(allFormats)
}
}
override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch {
val ids = mutableListOf<Long>()
runBlocking {
items.map { it.status = DownloadRepository.Status.Saved.toString() }
ids.addAll(downloadViewModel.repository.insertAll(items))
withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground()
}
val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()
.setInputData(
Data.Builder()
.putLongArray("ids", ids.toLongArray())
.putInt("id", id)
.build())
.addTag("updateFormats")
.build()
WorkManager.getInstance(requireContext()).enqueueUniqueWork(
id.toString(),
ExistingWorkPolicy.REPLACE,
workRequest
)
}
dismiss()
}
}
if (items[0].type == DownloadViewModel.Type.command){
if (type == DownloadViewModel.Type.command){
bottomAppBar.menu[3].icon?.alpha = 30
}
bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
@ -311,39 +268,38 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
audio!!.setOnClickListener {
items = downloadViewModel.switchDownloadType(items, DownloadViewModel.Type.audio).toMutableList()
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel()
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel()
}
}
}
video!!.setOnClickListener {
items = downloadViewModel.switchDownloadType(items, DownloadViewModel.Type.video).toMutableList()
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel()
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(DownloadViewModel.Type.video)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomSheet.cancel()
}
}
}
command!!.setOnClickListener {
lifecycleScope.launch {
items = withContext(Dispatchers.IO){
downloadViewModel.switchDownloadType(items, DownloadViewModel.Type.command).toMutableList()
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(DownloadViewModel.Type.command)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 30
bottomSheet.cancel()
}
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 30
bottomSheet.cancel()
}
}
@ -359,11 +315,15 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
R.id.format -> {
if (! items.all { it.type == items[0].type }){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show()
}else{
if (items.first().type == DownloadViewModel.Type.command){
lifecycleScope.launch {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
}
if (!res.first){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show()
}else{
if (res.second == DownloadViewModel.Type.command){
UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel) {
val format = Format(
it.first().title,
@ -375,15 +335,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
it.joinToString(" ") { c -> c.content }
)
items.forEach { f ->
f.format = format
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingCommandFormat(format)
}
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
}
}
}else{
lifecycleScope.launch {
}else{
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
}
val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){
flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value }
@ -402,182 +361,227 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
}
}
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()
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
}
if (!res.first) {
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)
when(res.second){
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
}
},
cropThumbClicked = {enabled ->
items.forEach {
it.audioPreferences.cropThumb = 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 = {},
updateDataClicked = {},
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")
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
}
)
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
)
}
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]) }
UiUtil.configureAudio(
sheetView,
requireActivity(),
items,
embedThumbClicked = {enabled ->
items.forEach {
it.audioPreferences.embedThumb = enabled
}
}
bottomSheet.dismiss()
},
cutClicked = {},
updateDataClicked = {},
filenameTemplateSet = { checked ->
items.forEach { it.customFileNameTemplate = checked }
},
saveSubtitlesClicked = {checked ->
items.forEach { it.videoPreferences.writeSubs = checked }
},
subtitleLanguagesSet = {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()
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
cropThumbClicked = {enabled ->
items.forEach {
it.audioPreferences.cropThumb = enabled
}
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
splitByChaptersClicked = {enabled ->
items.forEach {
it.audioPreferences.splitByChapters = enabled
}
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
filenameTemplateSet = {template ->
items.forEach {
it.customFileNameTemplate = template
}
requireActivity().lifecycleScope.launch {
items.forEach { downloadViewModel.updateDownload(it) }
}
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]) }
}
}
requireActivity().lifecycleScope.launch {
items.forEach { downloadViewModel.updateDownload(it) }
}
bottomSheet.dismiss()
},
cutClicked = {},
updateDataClicked = {},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
items.forEach { it.extraCommands = c }
requireActivity().lifecycleScope.launch {
items.forEach { downloadViewModel.updateDownload(it) }
}
bottomSheet.dismiss()
}
}
}
val bottomSheetDialog = AddExtraCommandsDialog(null, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
val bottomSheetDialog = AddExtraCommandsDialog(null, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
)
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
)
}
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)
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
}
)
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
)
}
DownloadViewModel.Type.command -> {
}
UiUtil.configureVideo(
sheetView,
requireActivity(),
items,
embedSubsClicked = {checked ->
items.forEach { it.videoPreferences.embedSubs = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
addChaptersClicked = {checked ->
items.forEach { it.videoPreferences.addChapters = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
splitByChaptersClicked = { checked ->
items.forEach { it.videoPreferences.splitByChapters = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
saveThumbnailClicked = {checked ->
items.forEach { it.SaveThumb = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
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]) }
}
}
requireActivity().lifecycleScope.launch {
items.forEach { downloadViewModel.updateDownload(it) }
}
bottomSheet.dismiss()
},
cutClicked = {},
updateDataClicked = {},
filenameTemplateSet = { checked ->
items.forEach { it.customFileNameTemplate = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
saveSubtitlesClicked = {checked ->
items.forEach { it.videoPreferences.writeSubs = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
saveAutoSubtitlesClicked = {checked ->
items.forEach { it.videoPreferences.writeAutoSubs = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
subtitleLanguagesSet = {value ->
items.forEach { it.videoPreferences.subsLanguages = value }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
removeAudioClicked = {checked ->
items.forEach { it.videoPreferences.removeAudio = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
items.forEach { it.extraCommands = c }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
bottomSheet.dismiss()
}
}
else -> {}
val bottomSheetDialog = AddExtraCommandsDialog(null, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
)
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
)
}
DownloadViewModel.Type.command -> {
}
else -> {}
}
}
}
}
}
true
}
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null) {
withContext(Dispatchers.Main){
kotlin.runCatching {
UiUtil.handleDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
res,
downloadViewModel,
historyViewModel)
}
}
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
errorMessage = null,
actions = null
)
lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest {
listAdapter.submitList(it)
withContext(Dispatchers.Main){
updateFileSize(it.map { it2 -> it2.format.filesize })
}
if (it.isNotEmpty()){
if (it.all { it2 -> it2.type == it[0].type }) {
withContext(Dispatchers.Main) {
bottomAppBar.menu[1].icon?.alpha = 255
if (it[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[3].icon?.alpha = 255
}
}
} else {
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}
}
}
}
@ -586,7 +590,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private fun updateFileSize(items: List<Long>){
if (items.all { it > 0L }){
filesize.visibility = View.VISIBLE
filesize.text = "${getString(R.string.file_size)}: ~ ${FileUtil.convertFileSize(items.sum())}"
filesize.text = "${getString(R.string.file_size)}: >~ ${FileUtil.convertFileSize(items.sum())}"
}else{
filesize.visibility = View.GONE
}
@ -603,18 +607,21 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
items.forEach {
it.downloadPath = result.data?.data.toString()
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingDownloadPath(result.data?.data.toString())
}
val path = FileUtil.formatPath(result.data!!.data.toString())
Snackbar.make(requireView(),getString(R.string.changed_path_for_everyone_to) + " " + path, Snackbar.LENGTH_LONG).show()
Toast.makeText(requireContext(),getString(R.string.changed_path_for_everyone_to) + " " + path, Toast.LENGTH_LONG).show()
}
}
override fun onButtonClick(itemURL: String) {
override fun onButtonClick(id: Long) {
lifecycleScope.launch {
val item = items.find { it.url == itemURL } ?: return@launch
val position = items.indexOf(item)
var item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -635,26 +642,31 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
audio!!.setOnClickListener {
items[position] = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.audio).first()
listAdapter.submitList(items.toList())
listAdapter.notifyItemChanged(position)
bottomSheet.cancel()
lifecycleScope.launch {
item = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.audio).first()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
bottomSheet.cancel()
}
}
video!!.setOnClickListener {
items[position] = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.video).first()
listAdapter.submitList(items.toList())
listAdapter.notifyItemChanged(position)
bottomSheet.cancel()
lifecycleScope.launch {
item = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.video).first()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
bottomSheet.cancel()
}
}
command!!.setOnClickListener {
lifecycleScope.launch {
items[position] = withContext(Dispatchers.IO){
downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.command).first()
item = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.command).first()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
listAdapter.submitList(items.toList())
listAdapter.notifyItemChanged(position)
bottomSheet.cancel()
}
}
@ -670,60 +682,71 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
override fun onCardClick(itemURL: String) {
override fun onCardClick(id: Long) {
lifecycleScope.launch{
val downloadItem = items.find { it.url == itemURL }
val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
val resultItem = withContext(Dispatchers.IO){
resultViewModel.getItemByURL(downloadItem!!.url)!!
resultViewModel.getItemByURL(downloadItem.url)!!
}
if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem!!, this@DownloadMultipleBottomSheetDialog)
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, this@DownloadMultipleBottomSheetDialog)
bottomSheet.show(parentFragmentManager, "configureDownloadSingleSheet")
}
}
}
override fun onDelete(itemURL: String) {
val deletedItem = items.find { it.url == itemURL } ?: return
val position = items.indexOf(deletedItem)
UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){
items.remove(deletedItem)
listAdapter.submitList(items.toList())
if (items.isNotEmpty()){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
items.add(position, deletedItem)
listAdapter.submitList(items.toList())
}.show()
}else{
dismiss()
override fun onDelete(id: Long) {
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
} ?: return@launch
UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){
lifecycleScope.launch {
val count = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloadsCount()
}
downloadViewModel.deleteDownload(id)
if (count > 1){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.insert(deletedItem)
}
}.show()
}else{
dismiss()
}
}
}
}
}
override fun onClick(p0: View?) {
}
override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) {
val i = items.indexOf(items.find { it.url == item.url })
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
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
listAdapter.notifyDataSetChanged()
}
}
if (items.count { it.type == DownloadViewModel.Type.command } == 0){
bottomAppBar.menu[2].icon?.alpha = 255
}else{
bottomAppBar.menu[2].icon?.alpha = 30
override fun onDismiss(dialog: DialogInterface) {
requireActivity().lifecycleScope.launch {
downloadViewModel.deleteProcessing()
}
listAdapter.submitList(items.toList())
super.onDismiss(dialog)
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
@ -734,22 +757,29 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
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]
items.remove(deletedItem)
listAdapter.submitList(items.toList())
if (items.isNotEmpty()){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
items.add(position, deletedItem)
listAdapter.submitList(items.toList())
}.show()
}else{
dismiss()
}
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
val count = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloadsCount()
}
withContext(Dispatchers.IO){
downloadViewModel.deleteDownload(deletedItem.id)
}
if (count > 1) {
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
downloadViewModel.insert(deletedItem)
}.show()
}else{
dismiss()
}
}
}
}

View file

@ -18,6 +18,7 @@ import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -34,23 +35,19 @@ import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM
import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
import com.google.android.material.textfield.TextInputLayout.EndIconMode
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class DownloadVideoFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment(), GUISync {
class DownloadVideoFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "", private var nonSpecific: Boolean = false) : Fragment(), GUISync {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
@ -67,6 +64,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
lateinit var downloadItem: DownloadItem
@SuppressLint("RestrictedApi")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -100,7 +98,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
try {
title = view.findViewById(R.id.title_textinput)
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
title.visibility = if (shownFields.contains("title") && !nonSpecific) View.VISIBLE else View.GONE
if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title)
title.endIconMode = END_ICON_NONE
@ -114,7 +112,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
})
author = view.findViewById(R.id.author_textinput)
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
author.visibility = if (shownFields.contains("author") && !nonSpecific) View.VISIBLE else View.GONE
if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author)
author.endIconMode = END_ICON_NONE
@ -200,6 +198,15 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val containers = requireContext().resources.getStringArray(R.array.video_containers)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
if (nonSpecific){
val param = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT,
1.0f
)
container.layoutParams = param
container.setPadding(0)
}
val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview)
var containerPreference = sharedPreferences.getString("video_format", "Default")
@ -329,6 +336,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
saveSubtitlesClicked = {
downloadItem.videoPreferences.writeSubs = it
},
saveAutoSubtitlesClicked = {
downloadItem.videoPreferences.writeAutoSubs = it
},
subtitleLanguagesSet = {
downloadItem.videoPreferences.subsLanguages = it
},

View file

@ -147,7 +147,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}
val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
if (!hasGenericFormats || items.isEmpty()) refreshBtn.visibility = View.GONE
if (!hasGenericFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) refreshBtn.visibility = View.GONE
refreshBtn.setOnClickListener {

View file

@ -0,0 +1,530 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipboardManager
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.RadioButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.ui.text.capitalize
import androidx.core.content.edit
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.w3c.dom.Text
import java.text.SimpleDateFormat
import java.time.Month
import java.time.format.TextStyle
import java.util.Calendar
import java.util.Locale
class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var observeSourcesViewModel: ObserveSourcesViewModel
private lateinit var historyViewModel: HistoryViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateViewModel : CommandTemplateViewModel
private lateinit var infoUtil: InfoUtil
private lateinit var sharedPreferences : SharedPreferences
private lateinit var view: View
private lateinit var type: Type
private var currentItem: ObserveSourcesItem? = null
private lateinit var okButton: MaterialButton
private lateinit var everyNr: TextInputLayout
private lateinit var title: TextInputLayout
private lateinit var url: TextInputLayout
private lateinit var everyCat: AutoCompleteTextView
private lateinit var everyTime: TextInputLayout
private lateinit var weekDays: ChipGroup
private lateinit var everyMonthDay: TextInputLayout
private lateinit var startTime: TextInputLayout
private lateinit var startMonth: TextInputLayout
private lateinit var endsNever: RadioButton
private lateinit var endsOn: RadioButton
private lateinit var endsAfter: RadioButton
private lateinit var endsOnTime: TextInputLayout
private lateinit var endsAfterNr: TextInputLayout
private lateinit var retryMissingDownloads: MaterialSwitch
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
observeSourcesViewModel = ViewModelProvider(requireActivity())[ObserveSourcesViewModel::class.java]
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
type = arguments?.getSerializable("type") as Type
currentItem = if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelable("item", ObserveSourcesItem::class.java)
}else{
arguments?.getParcelable<ObserveSourcesItem>("item")
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val downloadItem = getDownloadItem()
arguments?.putSerializable("type", downloadItem.type)
}
@SuppressLint("RestrictedApi", "InflateParams")
override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style)
view = LayoutInflater.from(context).inflate(R.layout.observe_sources_bottom_sheet, null)
dialog.setContentView(view)
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.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
}
}
tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager)
(viewPager2.getChildAt(0) as? RecyclerView)?.apply {
isNestedScrollingEnabled = false
overScrollMode = View.OVER_SCROLL_NEVER
}
var commandTemplateNr = 0
lifecycleScope.launch{
withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateViewModel.getTotalNumber()
if(commandTemplateNr <= 0){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f
}
}
}
val fragmentManager = parentFragmentManager
fragmentAdapter = DownloadFragmentAdapter(
fragmentManager,
lifecycle,
ResultItem(0, "", "", "", "", "", "", "", mutableListOf(), "", null, null, null, 0),
currentItem?.downloadItemTemplate,
true
)
viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false
when(type) {
Type.audio -> {
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
}
Type.video -> {
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
else -> {
tabLayout.getTabAt(2)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
}
}
sharedPreferences.edit(commit = true) {
putString("last_used_download_type",
type.toString())
}
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
if (tab!!.position == 2 && commandTemplateNr == 0){
tabLayout.selectTab(tabLayout.getTabAt(1))
val s = Snackbar.make(view, getString(R.string.add_template_first), Snackbar.LENGTH_LONG)
val snackbarView: View = s.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
s.setAction(R.string.new_template){
UiUtil.showCommandTemplateCreationOrUpdatingSheet(item = null, context = requireActivity(), lifeCycle = this@ObserveSourcesBottomSheetDialog, commandTemplateViewModel = commandTemplateViewModel){
commandTemplateNr = 1
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 1f
tabLayout.selectTab(tabLayout.getTabAt(2))
}
}
s.show()
}
else{
viewPager2.setCurrentItem(tab.position, false)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
viewPager2.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
tabLayout.selectTab(tabLayout.getTabAt(position))
runCatching {
sharedPreferences.edit(commit = true) {
putString("last_used_download_type",
listOf(Type.audio, Type.video, Type.command)[position].toString())
}
}
}
})
viewPager2.setPageTransformer(BackgroundToForegroundPageTransformer())
title = view.findViewById(R.id.title_textinput)
url = view.findViewById(R.id.url_textinput)
everyNr = view.findViewById(R.id.every_textinput)
everyCat = view.findViewById(R.id.everyCat)
everyTime = view.findViewById(R.id.every_time)
weekDays = view.findViewById(R.id.weekdays)
everyMonthDay = view.findViewById(R.id.everyMonthDay_textInput)
startTime = view.findViewById(R.id.start_time)
startMonth = view.findViewById(R.id.startsMonth_textInput)
endsNever = view.findViewById(R.id.never)
endsOn = view.findViewById(R.id.on)
endsOnTime = view.findViewById(R.id.on_date)
endsAfter = view.findViewById(R.id.after)
endsAfterNr = view.findViewById(R.id.after_nr)
retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads)
okButton = view.findViewById(R.id.okButton)
title.editText!!.setText(currentItem?.name ?: "")
title.editText?.doAfterTextChanged { checkIfValid() }
url.apply {
editText!!.setText(currentItem?.url ?: "")
editText?.doAfterTextChanged {
checkIfValid()
if (url.editText!!.text.isNotEmpty()){
url.endIconDrawable = AppCompatResources.getDrawable(requireActivity(), R.drawable.ic_delete_all)
}else{
url.endIconDrawable = AppCompatResources.getDrawable(requireActivity(), R.drawable.ic_clipboard)
}
}
setEndIconOnClickListener {
if(url.editText!!.text.isEmpty()){
val clipboard: ClipboardManager =
requireActivity().getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
url.editText!!.setText(clipboard.primaryClip?.getItemAt(0)?.text)
}else{
url.editText!!.setText("")
}
}
}
everyNr.editText?.setText(currentItem?.everyNr?.toString() ?: "1")
everyNr.editText?.doAfterTextChanged { checkIfValid() }
val cats = ObserveSourcesRepository.everyCategoryName.map { getString(it.value) }
val adapter = ArrayAdapter(requireActivity(),android.R.layout.simple_dropdown_item_1line, cats)
everyCat.doAfterTextChanged {
weekDays.isVisible = it.toString() == cats[1]
startMonth.isVisible = it.toString() == cats[2]
startTime.isVisible = !startMonth.isVisible
everyMonthDay.isVisible = startMonth.isVisible
}
everyCat.setAdapter(adapter)
if (currentItem != null){
val idx = ObserveSourcesRepository.EveryCategory.values().indexOf(currentItem!!.everyCategory)
everyCat.setText(cats[idx], false)
}else{
everyCat.setText(cats.first(), false)
}
everyTime.apply {
editText?.apply {
isFocusable = false
isClickable = false
setOnClickListener{
UiUtil.showTimePicker(fragmentManager){
everyTime.editText?.setText(
SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "HHmm"), Locale.getDefault()).format(it.timeInMillis)
)
everyTime.tag = it.timeInMillis
checkIfValid()
}
}
if (currentItem != null){
setText(SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "HHmm"), Locale.getDefault()).format(currentItem?.everyTime))
everyTime.tag = currentItem?.everyTime
checkIfValid()
}
}
}
val calendar = Calendar.getInstance()
//using 2024 because it starts with a monday
calendar.set(Calendar.YEAR, 2024) // Set the year
calendar.set(Calendar.MONTH, Calendar.JANUARY) // Set the month (Note: Months are zero-based)
calendar.set(Calendar.DAY_OF_MONTH, 1) // Set the day of the month
calendar.firstDayOfWeek = Calendar.MONDAY
val daysOfWeek = mutableListOf<String>()
val daysOfWeekTags = mutableListOf<String>()
for (i in 0..6) {
val dayName = calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault())!!
daysOfWeek.add(dayName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() })
daysOfWeekTags.add(calendar.get(Calendar.DAY_OF_WEEK).toString())
calendar.add(Calendar.DAY_OF_WEEK, 1)
}
weekDays.children.forEachIndexed { index, view ->
(view as Chip).text = daysOfWeek[index]
(view as Chip).tag = daysOfWeekTags[index]
}
if (currentItem != null){
weekDays.children.forEach { view ->
(view as Chip).isChecked = currentItem!!.everyWeekDay.contains(view.tag.toString())
}
}
val everyMonthDayAutoCompleteTextView = view.findViewById<AutoCompleteTextView>(R.id.everyMonthDay)
everyMonthDayAutoCompleteTextView.setText(
currentItem?.everyMonthDay?.toString() ?: Calendar.getInstance().get(Calendar.DAY_OF_MONTH).toString()
, false)
val months = mutableListOf<String>()
for (i in 0..11){
months.add(calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault())!!.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() })
calendar.add(Calendar.MONTH, 1)
}
val startMonthAutoCompleteTextView = startMonth.findViewById<AutoCompleteTextView>(R.id.startsMonth)
startMonthAutoCompleteTextView.setAdapter(ArrayAdapter(requireActivity(),android.R.layout.simple_dropdown_item_1line, months))
if (currentItem == null){
startMonthAutoCompleteTextView.setText(months.first(), false)
}else{
calendar.set(Calendar.MONTH, Month.values().indexOf(currentItem!!.startsMonth))
startMonthAutoCompleteTextView.setText(calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault())!!.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, false)
}
startTime.apply {
editText?.apply {
isFocusable = false
isClickable = true
setOnClickListener {
UiUtil.showDatePickerOnly(fragmentManager){
startTime.editText?.setText(
SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy"), Locale.getDefault()).format(it.timeInMillis)
)
startTime.tag = it.timeInMillis
checkIfValid()
}
}
val now = Calendar.getInstance().timeInMillis
startTime.editText?.setText(
SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy"), Locale.getDefault()).format(now)
)
startTime.tag = now
}
}
endsOnTime.apply {
editText?.apply {
isFocusable = false
isClickable = true
setText(
SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy"), Locale.getDefault()).format(
Calendar.getInstance().apply { add(Calendar.MONTH, 5) }.timeInMillis)
)
tag = Calendar.getInstance().apply { add(Calendar.MONTH, 5) }.timeInMillis
setOnClickListener {
UiUtil.showDatePickerOnly(fragmentManager){
endsOnTime.editText?.setText(
SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy"), Locale.getDefault()).format(it.timeInMillis)
)
endsOnTime.tag = it.timeInMillis
checkIfValid()
}
}
if (currentItem != null && currentItem!!.endsDate > 0){
setText(SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "HHmm"), Locale.getDefault()).format(currentItem?.endsDate))
endsOnTime.tag = currentItem?.endsDate
checkIfValid()
}
}
}
endsAfterNr.editText?.setText("10")
endsAfterNr.editText?.doAfterTextChanged { checkIfValid() }
if (currentItem != null && currentItem!!.endsAfterCount > 0){
endsAfterNr.editText?.setText(currentItem!!.endsAfterCount.toString())
}
endsNever.setOnClickListener {
endsOn.isChecked = false
endsAfter.isChecked = false
checkIfValid()
}
endsOn.setOnClickListener {
endsNever.isChecked = false
endsAfter.isChecked = false
checkIfValid()
}
if ((currentItem?.endsDate ?: 0) > 0){
endsOn.performClick()
}
endsAfter.setOnClickListener {
endsOn.isChecked = false
endsNever.isChecked = false
checkIfValid()
}
if ((currentItem?.endsAfterCount ?: 0) > 0){
endsAfter.performClick()
}
if (currentItem != null) okButton.text = getString(R.string.update)
okButton.setOnClickListener {
lifecycleScope.launch {
val item: DownloadItem = getDownloadItem()
var ends = 0L
if(endsOn.isChecked){
ends = endsOnTime.tag as Long
}
var endsAfterCount = 0
if(endsAfter.isChecked){
endsAfterCount = endsAfterNr.editText!!.text.toString().toInt()
}
val observeItem = ObserveSourcesItem(
currentItem?.id ?: 0,
title.editText!!.text.toString(),
url.editText!!.text.toString(),
item,
ObserveSourcesRepository.SourceStatus.ACTIVE,
everyNr.editText!!.text.toString().toInt(),
ObserveSourcesRepository.EveryCategory.values()[adapter.getPosition(everyCat.text.toString())],
weekDays.children.filter { (it as Chip).isChecked }.map { it.tag.toString() }.toList(),
everyMonthDay.editText!!.text.toString().toInt(),
everyTime.tag as Long,
startTime.tag as Long,
Month.values()[startMonthAutoCompleteTextView.selectionStart],
ends,
endsAfterCount,
0,
retryMissingDownloads.isChecked,
mutableListOf()
)
withContext(Dispatchers.IO){
observeSourcesViewModel.insert(observeItem)
}
dismiss()
}
}
checkIfValid()
}
private fun checkIfValid(){
var valid = title.editText?.text?.isNotBlank() == true
&& url.editText?.text?.isNotBlank() == true
&& everyNr.editText?.text?.isNotBlank() == true
&& everyTime.editText?.text?.isNotBlank() == true
&& startTime.editText?.text?.isNotBlank() == true
if (endsOn.isChecked && endsOnTime.editText?.text?.isBlank() == true) valid = false
else if (endsAfter.isChecked && endsAfterNr.editText?.text?.isBlank() == true) valid = false
okButton.isEnabled = valid
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
return when(selectedTabPosition){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.downloadItem
}
else -> {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.downloadItem
}
}
}
}

View file

@ -22,6 +22,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.PlaylistAdapter
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
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity
@ -31,6 +32,7 @@ import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -197,11 +199,13 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
downloadItems.add(i)
}
withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog, bundleOf(
Pair("downloads", downloadItems)
))
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadItems)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("type", downloadItems[0].type)
))
}
dismiss()

View file

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -17,8 +18,10 @@ import androidx.viewpager2.widget.ViewPager2
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
@ -136,6 +139,24 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.deleteSaved()
}
}
R.id.copy_urls -> {
lifecycleScope.launch {
val tabStatus = mapOf(
0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused, DownloadRepository.Status.PausedReQueued),
1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Queued),
2 to listOf(DownloadRepository.Status.Cancelled),
3 to listOf(DownloadRepository.Status.Error),
4 to listOf(DownloadRepository.Status.Saved),
)
tabStatus[tabLayout.selectedTabPosition]?.apply {
val urls = withContext(Dispatchers.IO){
downloadViewModel.getURLsByStatus(this@apply)
}
UiUtil.copyToClipboard(urls.joinToString("\n"), requireActivity())
}
}
}
}
}catch (e: Exception){
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()

View file

@ -60,6 +60,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.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@ -80,7 +81,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private var fragmentContext: Context? = null
private var layoutinflater: LayoutInflater? = null
private var topAppBar: MaterialToolbar? = null
private var recyclerView: RecyclerView? = null
private lateinit var recyclerView: RecyclerView
private var historyAdapter: HistoryAdapter? = null
private var sortSheet: BottomSheetDialog? = null
private var uiHandler: Handler? = null
@ -128,8 +129,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
requireActivity()
)
recyclerView = view.findViewById(R.id.recyclerviewhistorys)
recyclerView?.adapter = historyAdapter
recyclerView?.enableFastScroll()
recyclerView.adapter = historyAdapter
recyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("history")){
@ -137,7 +138,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
itemTouchHelper.attachToRecyclerView(recyclerView)
}
recyclerView?.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
noResults?.visibility = GONE
@ -185,11 +186,11 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
lifecycleScope.launch{
downloadViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null) {
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
withContext(Dispatchers.Main){
kotlin.runCatching {
UiUtil.handleDownloadsResponse(
UiUtil.handleExistingDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
@ -198,9 +199,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel)
}
}
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
errorMessage = null,
actions = null
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
mutableListOf(),
mutableListOf()
)
}
}
@ -396,6 +397,17 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
command.isChecked = false
}
}
val notDeleted = fragmentView!!.findViewById<Chip>(R.id.not_deleted_chip)
notDeleted.setOnClickListener {
if (notDeleted.isChecked) {
historyViewModel.setNotDeleted(true)
notDeleted.isChecked = true
} else {
historyViewModel.setNotDeleted(false)
notDeleted.isChecked = false
}
}
}
private fun updateWebsiteChips(list : List<HistoryItem>) {
@ -577,7 +589,18 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyAdapter!!.notifyItemChanged(position)
UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(),
delete = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
lifecycleScope.launch {
withContext(Dispatchers.IO){
historyViewModel.delete(item, deleteFile)
}
if (!deleteFile){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
historyViewModel.insert(deletedItem)
}.show()
}
}
})
}
ItemTouchHelper.RIGHT -> {

View file

@ -38,7 +38,6 @@ import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository.CommandTemplateSortType
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar

View file

@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip
@ -39,6 +39,7 @@ import com.google.android.material.textfield.TextInputLayout
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@ -162,38 +163,51 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
}
private fun showDialog(url: String){
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.cookies))
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 = "URL"
val text = if (url.isNullOrBlank()) "https://" else url
editText.setText(text)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
getString(R.string.get_cookies)
) { dialog: DialogInterface?, which: Int ->
val myIntent = Intent(requireContext(), WebViewActivity::class.java)
myIntent.putExtra("url", editText.text.toString())
startActivity(myIntent)
}
lifecycleScope.launch {
var item = withContext(Dispatchers.IO){
cookiesViewModel.getByURL(url)
}
// 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 ->
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.cookies))
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 = "URL"
val text = if (url.isNullOrBlank()) "https://" else url
editText.setText(text)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setNeutralButton(
getString(android.R.string.copy)
) { dialog: DialogInterface?, which: Int ->
UiUtil.copyToClipboard(item.content, requireActivity())
}
builder.setPositiveButton(
getString(R.string.get_cookies)
) { dialog: DialogInterface?, which: Int ->
val myIntent = Intent(requireContext(), WebViewActivity::class.java)
myIntent.putExtra("url", editText.text.toString())
startActivity(myIntent)
}
// 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 = mainActivity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
dialog.show()
val imm = mainActivity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
companion object {

View file

@ -35,6 +35,7 @@ class MoreFragment : Fragment() {
private lateinit var commandTemplates: TextView
private lateinit var downloadQueue: TextView
private lateinit var cookies: TextView
private lateinit var observeSources: TextView
private lateinit var terminateApp: TextView
private lateinit var settings: TextView
private lateinit var mainActivity: MainActivity
@ -58,6 +59,7 @@ class MoreFragment : Fragment() {
commandTemplates = view.findViewById(R.id.command_templates)
downloadQueue = view.findViewById(R.id.download_queue)
cookies = view.findViewById(R.id.cookies)
observeSources = view.findViewById(R.id.observe_sources)
terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings)
@ -82,6 +84,10 @@ class MoreFragment : Fragment() {
findNavController().navigate(R.id.cookiesFragment)
}
observeSources.setOnClickListener {
findNavController().navigate(R.id.observeSourcesFragment)
}
terminateApp.setOnClickListener {
if (mainSharedPreferences.getBoolean("ask_terminate_app", true)){
var doNotShowAgain = false

View file

@ -0,0 +1,225 @@
package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
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.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver
import com.deniscerri.ytdlnis.ui.adapter.ObserveSourcesAdapter
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip
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.launch
import kotlinx.coroutines.withContext
class ObserveSourcesFragment : Fragment(), ObserveSourcesAdapter.OnItemClickListener {
private lateinit var recyclerView: RecyclerView
private lateinit var listAdapter: ObserveSourcesAdapter
private lateinit var topAppBar: MaterialToolbar
private lateinit var observeSourcesViewModel: ObserveSourcesViewModel
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var noResults : RelativeLayout
private lateinit var mainActivity: MainActivity
private lateinit var preferences: SharedPreferences
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mainActivity = activity as MainActivity
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
return inflater.inflate(R.layout.fragment_observe_sources, container, false)
}
@SuppressLint("RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
topAppBar = view.findViewById(R.id.toolbar)
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
noResults = view.findViewById(R.id.no_results)
listAdapter =
ObserveSourcesAdapter(
this,
mainActivity
)
val newSource = view.findViewById<Chip>(R.id.newSource)
recyclerView = view.findViewById(R.id.source_recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = listAdapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("cookies")){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
observeSourcesViewModel = ViewModelProvider(this)[ObserveSourcesViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
observeSourcesViewModel.items.observe(viewLifecycleOwner) {
if (it.isEmpty()) noResults.visibility = View.VISIBLE
else noResults.visibility = View.GONE
listAdapter.submitList(it)
}
initMenu()
newSource.setOnClickListener {
showDialog(null)
}
}
private fun initMenu() {
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
R.id.delete_sources -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.confirm_delete_history))
deleteDialog.setMessage(getString(R.string.confirm_delete_sources_desc))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
observeSourcesViewModel.deleteAll()
}
deleteDialog.show()
}
}
true
}
}
private fun showDialog(url: String?){
lifecycleScope.launch {
val bundle = Bundle()
bundle.putSerializable("type", downloadViewModel.getDownloadType(
DownloadViewModel.Type.valueOf(preferences.getString("download_type", "auto")!!), "")
)
if (url != null){
val item = withContext(Dispatchers.IO){
observeSourcesViewModel.getByURL(url)
}
bundle.putParcelable("item", item)
}
findNavController().navigate(R.id.observeSourcesBottomSheetDialog, bundle)
}
}
override fun onItemSearch(item: ObserveSourcesItem) {
runCatching {
val intent = Intent(context, ObserveAlarmReceiver::class.java)
intent.putExtra("id", item.id)
requireActivity().sendBroadcast(intent)
}.onFailure {
Snackbar.make(requireView(), it.message.toString(), Snackbar.LENGTH_SHORT).show()
}
}
override fun onItemStart(item: ObserveSourcesItem, position: Int) {
lifecycleScope.launch {
withContext(Dispatchers.IO){
item.status = ObserveSourcesRepository.SourceStatus.ACTIVE
item.runCount = 0
observeSourcesViewModel.insert(item)
}
listAdapter.notifyItemChanged(position)
}
}
override fun onItemClick(item: ObserveSourcesItem) {
showDialog(item.url)
}
override fun onDelete(item: ObserveSourcesItem) {
UiUtil.showGenericDeleteDialog(requireContext(), item.name){
observeSourcesViewModel.delete(item)
}
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT ) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val url = viewHolder.itemView.tag.toString()
when (direction) {
ItemTouchHelper.LEFT -> {
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
observeSourcesViewModel.getByURL(url)
}
listAdapter.notifyItemChanged(position)
onDelete(deletedItem)
}
}
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
RecyclerViewSwipeDecorator.Builder(
context,
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
.addSwipeLeftBackgroundColor(Color.RED)
.addSwipeLeftActionIcon(R.drawable.baseline_delete_24)
.create()
.decorate()
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.ActionBar
import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
@ -13,10 +14,12 @@ import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.core.content.edit
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
@ -39,6 +42,7 @@ class DownloadLogFragment : Fragment() {
private lateinit var copyLog : ExtendedFloatingActionButton
private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel
private lateinit var sharedPreferences: SharedPreferences
override fun onCreateView(
inflater: LayoutInflater,
@ -47,6 +51,7 @@ class DownloadLogFragment : Fragment() {
): View? {
mainActivity = activity as MainActivity
mainActivity.hideBottomNavigation()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
return inflater.inflate(R.layout.fragment_download_log, container, false)
}
@ -141,9 +146,13 @@ class DownloadLogFragment : Fragment() {
slider?.apply {
this.valueFrom = 0f
this.valueTo = 10f
this.value = 2f
this.value = sharedPreferences.getFloat("log_zoom", 2f)
content.setCustomTextSize(this.value + 13f)
this.addOnChangeListener { slider, value, fromUser ->
content.setCustomTextSize(value + 13f)
sharedPreferences.edit(true){
putFloat("log_zoom", value)
}
}
}

View file

@ -7,7 +7,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers.AlarmStartReceiver
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import java.util.Calendar

View file

@ -21,6 +21,7 @@ import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
@ -278,10 +279,15 @@ class TerminalFragment : Fragment() {
slider?.apply {
this.valueFrom = 0f
this.valueTo = 10f
this.value = 2f
this.value = sharedPreferences.getFloat("terminal_zoom", 2f)
output?.setCustomTextSize(this.value + 13f)
input?.setCustomTextSize(this.value + 13f)
this.addOnChangeListener { slider, value, fromUser ->
output?.setCustomTextSize(value + 13f)
input?.setCustomTextSize(value + 13f)
sharedPreferences.edit(true){
putFloat("terminal_zoom", value)
}
}
}
}

View file

@ -0,0 +1,92 @@
package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.os.Looper
import android.widget.Toast
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver
import com.deniscerri.ytdlnis.util.Extensions.closestValue
import com.deniscerri.ytdlnis.work.DownloadWorker
import java.time.Month
import java.util.Calendar
import java.util.concurrent.TimeUnit
object DownloadUtil {
@SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context)
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 = 0L
if (queuedItems.isNotEmpty()){
delay = if (queuedItems[0].downloadStartTime != 0L){
queuedItems[0].downloadStartTime.minus(currentTime)
} else 0
if (delay <= 60000L) delay = 0L
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){
Looper.prepare().run {
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}
}
}
fun cancelObservationTaskByID(context: Context, id: Long){
val alarmManager = context.getSystemService(AlarmManager::class.java)
alarmManager.cancel(
PendingIntent.getBroadcast(
context,
id.toInt(),
Intent(context, ObserveAlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
}
}

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.util
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
@ -11,25 +12,26 @@ import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.animation.Interpolator
import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView
import androidx.annotation.Px
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
@ -37,7 +39,9 @@ import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import org.json.JSONObject
import java.io.File
import java.util.Locale
import java.util.regex.Pattern
import kotlin.math.abs
object Extensions {
@ -110,6 +114,7 @@ object Extensions {
drawable.paint.color = context.getColor(android.R.color.transparent)
FastScrollerBuilder(this)
.useMd2Style()
.setTrackDrawable(drawable)
.build()
}
@ -187,4 +192,41 @@ object Extensions {
this.setTextSize(TypedValue.COMPLEX_UNIT_SP, newSize)
}
fun Int.toStringDuration(locale: Locale): String {
var format = String.format(
locale,
"%02d:%02d:%02d",
this / 3600,
this % 3600 / 60,
this % 60
)
// 00:00:00
if (this < 600) format = format.substring(4) else if (this < 3600) format =
format.substring(3) else if (this < 36000) format = format.substring(1)
return format
}
fun View.popup(){
val animator = ValueAnimator.ofFloat( 0.75f, 1f)
animator.addUpdateListener { animation: ValueAnimator ->
val value = animation.animatedValue as Float
this.scaleX = value
this.scaleY = value
}
animator.interpolator = Extensions.CustomInterpolator()
animator.setDuration(300)
animator.start()
}
fun List<String>.closestValue(value: String) = minBy { abs(value.toInt() - it.toInt()) }
class CustomInterpolator : Interpolator {
override fun getInterpolation(input: Float): Float {
// Adjust this curve as needed for desired animation feel
return (Math.pow((input - 1).toDouble(), 5.0) + 1).toFloat()
}
}
}

View file

@ -286,6 +286,10 @@ object FileUtil {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/Command"
}
fun getDownloadArchivePath(context: Context) : String {
return context.cacheDir.absolutePath + "/download_archive.txt"
}
fun getDefaultTerminalPath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/TERMINAL_CACHE"
}

View file

@ -20,6 +20,7 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.getIntByAny
import com.deniscerri.ytdlnis.util.Extensions.getStringByAny
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
@ -122,7 +123,6 @@ class InfoUtil(private val context: Context) {
val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))
itm?.playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id"
itm?.playlistIndex = (i+1)
items.add(itm)
}
}
@ -187,7 +187,7 @@ class InfoUtil(private val context: Context) {
Html.fromHtml(obj.getString("uploaderName").toString()).toString()
}
val duration = formatIntegerDuration(obj.getInt("duration"), Locale.US)
val duration = obj.getInt("duration").toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList()
@ -206,6 +206,9 @@ class InfoUtil(private val context: Context) {
}else{
formatObj.format_note = formatObj.format_note + " Audio"
}
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) {
e.printStackTrace()
@ -223,6 +226,9 @@ class InfoUtil(private val context: Context) {
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.vcodec = format.getString("codec")
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) {
e.printStackTrace()
}
@ -453,6 +459,10 @@ class InfoUtil(private val context: Context) {
request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
@ -505,6 +515,10 @@ class InfoUtil(private val context: Context) {
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
@ -588,6 +602,10 @@ class InfoUtil(private val context: Context) {
request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
@ -625,7 +643,7 @@ class InfoUtil(private val context: Context) {
val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
}
var thumb: String? = ""
@ -724,6 +742,7 @@ class InfoUtil(private val context: Context) {
}catch (ignored: Exception){}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.format_note == null) continue
if (format.has("format_note")){
if (!formatProper!!.format_note.contains("audio only", true)) {
@ -736,10 +755,20 @@ class InfoUtil(private val context: Context) {
}
if (formatProper.format_note == "storyboard") continue
formatProper.container = format.getString("ext")
if (formatProper.tbr == "None") formatProper.tbr = ""
if (!formatProper.tbr.isNullOrBlank()){
formatProper.tbr = formatProper.tbr + "k"
}
if(formatProper.vcodec.isEmpty() || formatProper.vcodec == "null"){
if(formatProper.acodec.isEmpty() || formatProper.acodec == "null"){
formatProper.vcodec = format.getStringByAny("video_ext", "ext").ifEmpty { "unknown" }
}
}
formats.add(formatProper)
}
}
return formats
}
@ -751,6 +780,10 @@ class InfoUtil(private val context: Context) {
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
@ -780,7 +813,7 @@ class InfoUtil(private val context: Context) {
var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
}
var thumb: String? = ""
@ -871,19 +904,7 @@ class InfoUtil(private val context: Context) {
return duration
}
fun formatIntegerDuration(dur: Int, locale: Locale): String {
var format = String.format(
locale,
"%02d:%02d:%02d",
dur / 3600,
dur % 3600 / 60,
dur % 60
)
// 00:00:00
if (dur < 600) format = format.substring(4) else if (dur < 3600) format =
format.substring(3) else if (dur < 36000) format = format.substring(1)
return format
}
fun getStreamingUrlAndChapters(url: String) : MutableList<String?> {
try {
@ -904,6 +925,10 @@ class InfoUtil(private val context: Context) {
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
@ -955,11 +980,16 @@ class InfoUtil(private val context: Context) {
@SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || downloadItem.playlistIndex == null){
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
YoutubeDLRequest(downloadItem.url)
}else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
if(downloadItem.playlistIndex == null){
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
addOption("--match-filter", "id~='${matchPortion}'")
}else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
}
}
}
@ -1006,6 +1036,9 @@ class InfoUtil(private val context: Context) {
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
if (sharedPreferences.getBoolean("restrict_filenames", true)) request.addOption("--restrict-filenames")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
@ -1125,6 +1158,10 @@ class InfoUtil(private val context: Context) {
}
}
if (sharedPreferences.getBoolean("download_archive", false)){
request.addOption("--download-archive", FileUtil.getDownloadArchivePath(context))
}
val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec)
val aCodecPref = runCatching { context.getStringArray(R.array.audio_codec_values_ytdlp)[aCodecPrefIndex] }.getOrElse { "" }
@ -1365,17 +1402,23 @@ class InfoUtil(private val context: Context) {
request.addOption("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", subFormat ?: "srt")
}
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
}
if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs){
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
if(downloadItem.videoPreferences.writeAutoSubs){
request.addOption("--write-auto-subs")
}
if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs || downloadItem.videoPreferences.writeAutoSubs){
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
}
if (downloadItem.videoPreferences.removeAudio){
request.addOption("--ppa", "ffmpeg:-an")
}

View file

@ -126,6 +126,27 @@ class NotificationUtil(var context: Context) {
.build()
}
fun createObserveSourcesNotification(title: String) : Notification {
val notificationBuilder = getBuilder(DOWNLOAD_WORKER_CHANNEL_ID)
return notificationBuilder
.setContentTitle(resources.getString(R.string.observe_sources))
.setContentText(title)
.setOngoing(true)
.setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(
BitmapFactory.decodeResource(
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?,
title: String?,

View file

@ -25,6 +25,7 @@ import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
@ -33,14 +34,18 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.ui.text.toUpperCase
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.FileProvider
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
@ -53,12 +58,18 @@ import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.AlreadyExistsAdapter
import com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
import com.deniscerri.ytdlnis.util.Extensions.dp
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
@ -396,6 +407,14 @@ object UiUtil {
val item = context.layoutInflater.inflate(R.layout.filepath_card, list, false)
item.apply {
findViewById<TextView>(R.id.file_name).text = file.nameWithoutExtension
findViewById<TextView>(R.id.duration).apply {
val duration = file.getMediaDuration(context)
isVisible = duration > 0
text = duration.toStringDuration(Locale.US)
}
findViewById<TextView>(R.id.filesize).text = FileUtil.convertFileSize(file.length())
findViewById<TextView>(R.id.extension).text = file.extension.uppercase()
if (!file.exists()){
@ -456,6 +475,25 @@ object UiUtil {
}
}
fun showDatePickerOnly(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){
val date = Calendar.getInstance()
val datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(
CalendarConstraints.Builder()
.setValidator(DateValidatorPointForward.now())
.build()
)
.setSelection(MaterialDatePicker.todayInUtcMilliseconds())
.build()
datePicker.addOnPositiveButtonClickListener{
date.timeInMillis = it
onSubmit(date)
}
datePicker.show(fragmentManager, "datepicker")
}
fun showDatePicker(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){
val currentDate = Calendar.getInstance()
val date = Calendar.getInstance()
@ -869,6 +907,7 @@ object UiUtil {
val formatnoteParent = bottomSheet.findViewById<ConstraintLayout>(R.id.format_note_parent)
val fpsParent = bottomSheet.findViewById<ConstraintLayout>(R.id.fps_parent)
val asrParent = bottomSheet.findViewById<ConstraintLayout>(R.id.asr_parent)
val bitrateParent = bottomSheet.findViewById<ConstraintLayout>(R.id.bitrate_parent)
val clicker = View.OnClickListener {
@ -954,6 +993,13 @@ object UiUtil {
asrParent?.setOnLongClickListener(longClicker)
}
if (format.tbr.isNullOrBlank()) bitrateParent?.visibility = View.GONE
else{
bitrateParent?.findViewById<TextView>(R.id.bitrate_value)?.text = format.tbr
bitrateParent?.setOnClickListener(clicker)
bitrateParent?.setOnLongClickListener(longClicker)
}
bottomSheet.show()
@ -1139,6 +1185,7 @@ object UiUtil {
cutClicked: (VideoCutListener) -> Unit,
filenameTemplateSet: (String) -> Unit,
saveSubtitlesClicked: (Boolean) -> Unit,
saveAutoSubtitlesClicked: (Boolean) -> Unit,
subtitleLanguagesSet: (String) -> Unit,
removeAudioClicked: (Boolean) -> Unit,
extraCommandsClicked: () -> Unit,
@ -1148,6 +1195,7 @@ object UiUtil {
val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles)
val saveSubtitles = view.findViewById<Chip>(R.id.save_subtitles)
val saveAutoSubtitles = view.findViewById<Chip>(R.id.save_auto_subtitles)
val subtitleLanguages = view.findViewById<Chip>(R.id.subtitle_languages)
embedSubs!!.isChecked = items.all { it.videoPreferences.embedSubs }
@ -1173,6 +1221,8 @@ object UiUtil {
if (splitByChapters.isChecked){
items.forEach { it.videoPreferences.addChapters = false }
addChapters.isChecked = false
addChapters.isEnabled = false
addChaptersClicked(false)
}
splitByChapters.setOnClickListener {
if (splitByChapters.isChecked){
@ -1230,7 +1280,7 @@ object UiUtil {
}
val cut = view.findViewById<Chip>(R.id.cut)
if (items.size > 1) cut.isVisible = false
if (items.size > 1 || items.first().url.isEmpty()) cut.isVisible = false
else{
if(items[0].duration.isNotEmpty()){
val downloadItem = items[0]
@ -1306,6 +1356,10 @@ object UiUtil {
saveSubtitlesClicked(saveSubtitles.isChecked)
}
saveAutoSubtitles.setOnCheckedChangeListener { _, _ ->
saveAutoSubtitlesClicked(saveAutoSubtitles.isChecked)
}
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
subtitleLanguages.setOnClickListener {
val currentSubtitleLang = if (items.size == 1 || items.all { it.videoPreferences.subsLanguages == items[0].videoPreferences.subsLanguages }){
@ -1425,7 +1479,7 @@ object UiUtil {
}
val cut = view.findViewById<Chip>(R.id.cut)
if (items.size > 1) cut.isVisible = false
if (items.size > 1 || items.first().url.isEmpty()) cut.isVisible = false
else{
val downloadItem = items[0]
if (downloadItem.duration.isNotEmpty()){
@ -1555,136 +1609,163 @@ object UiUtil {
}
@SuppressLint("SetTextI18n")
fun handleDownloadsResponse(context: Activity, lifecycleScope: CoroutineScope, supportFragmentManager: FragmentManager, it: DownloadViewModel.DownloadsUiState, downloadViewModel: DownloadViewModel, historyViewModel: HistoryViewModel){
val downloadAnywayAction = it.actions?.first { it.second == DownloadViewModel.DownloadsAction.DOWNLOAD_ANYWAY}
if (downloadAnywayAction != null){
if (downloadAnywayAction.third == null) return
val downloads =downloadAnywayAction.third!!.toMutableList()
val ids = downloadAnywayAction.third!!.map { it.id }
val titles = downloadAnywayAction.third!!.map { it.title }
fun handleExistingDownloadsResponse(context: Activity, lifecycleScope: CoroutineScope, supportFragmentManager: FragmentManager, it: DownloadViewModel.AlreadyExistsUIState, downloadViewModel: DownloadViewModel, historyViewModel: HistoryViewModel){
if (it.downloadItems.isEmpty() && it.historyItems.isEmpty()) return
val title = context.getString(R.string.download_already_exists)
val message = context.getString(R.string.download_already_exists_summary)
val title = context.getString(it.errorMessage!!.first)
val message = context.getString(it.errorMessage!!.second)
val sheet = BottomSheetDialog(context)
sheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
sheet.setContentView(R.layout.download_multiple_bottom_sheet)
sheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
text = title
textSize = 20f
}
sheet.findViewById<TextView>(R.id.bottom_sheet_subtitle)?.text = message
val errDialog = MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
var adapter: AlreadyExistsAdapter? = null
val list = mutableListOf<Pair<DownloadItem, Long?>>()
val alreadyExistsClickListener = object: AlreadyExistsAdapter.OnItemClickListener {
var dialog: Dialog? = null
val linearLayout = context.layoutInflater.inflate(R.layout.already_exists_card, null) as LinearLayout
ids.forEachIndexed { index, id ->
val alreadyExistsItem = context.layoutInflater.inflate(R.layout.already_exists_item, null)
alreadyExistsItem.tag = id.toString()
val ttle = alreadyExistsItem.findViewById<Button>(R.id.already_exists_title)
ttle.text = "${index + 1}. ${titles[index]}"
CoroutineScope(Dispatchers.IO).launch {
val editBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_edit)
var downloadItem: DownloadItem = downloadViewModel.getItemByID(id)
val historyItem: HistoryItem? = downloadViewModel.getHistoryItemById(id)
if (historyItem != null){
val idx = downloads.indexOfFirst { it.id == historyItem.id }
downloadItem = downloadViewModel.createDownloadItemFromHistory(historyItem)
downloadItem.id = historyItem.id
downloads[idx] = downloadItem
override fun onEditItem(downloadItem: DownloadItem, position: Int) {
val resultItem = downloadViewModel.createResultItemFromDownload(downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem
) {
val currentIndex = list.indexOfFirst { it.first.id == item.id }
val current = list[currentIndex]
list[currentIndex] = Pair(item, current.second)
adapter?.submitList(list)
adapter?.notifyItemChanged(position)
}
}
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, onItemUpdated)
bottomSheet.show(supportFragmentManager, "configureDownloadSingleSheet")
}
override fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?) {
showGenericDeleteDialog(context, downloadItem.title){
if (historyID == null){
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteDownload(downloadItem.id)
}
}
list.removeAll { it.first.id == downloadItem.id }
if (list.isEmpty()){
sheet.dismiss()
}
adapter?.notifyDataSetChanged()
}
}
override fun onShowHistoryItem(id: Long) {
lifecycleScope.launch {
val historyItem = withContext(Dispatchers.IO){
downloadViewModel.getHistoryItemById(id)
}
showHistoryItemDetailsCard(historyItem, context, isPresent = true,
removeItem = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
},
redownloadItem = { },
redownloadShowDownloadCard = {}
)
}
}
}
adapter = AlreadyExistsAdapter(alreadyExistsClickListener, context)
sheet.findViewById<RecyclerView>(R.id.downloadMultipleRecyclerview)?.apply {
layoutManager = GridLayoutManager(context, 1)
this.adapter = adapter
enableFastScroll()
setPadding(0,20,0,0)
}
CoroutineScope(Dispatchers.IO).launch {
it.downloadItems.forEach {
val downloadItem = downloadViewModel.getItemByID(it)
list.add(Pair(downloadItem, null))
adapter.submitList(list)
}
it.historyItems.forEach {
val historyItem = downloadViewModel.getHistoryItemById(it) ?: return@forEach
val downloadItem = downloadViewModel.createDownloadItemFromHistory(historyItem)
downloadItem.id = it
list.add(Pair(downloadItem, historyItem.id))
adapter.submitList(list)
}
}
sheet.findViewById<Button>(R.id.bottomsheet_download_button)?.apply {
setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
val items = list.map { it.first }
items.forEach { it.id = 0 }
downloadViewModel.queueDownloads(items, true)
withContext(Dispatchers.Main){
editBtn.visibility = View.VISIBLE
}
editBtn.setOnClickListener {
val resultItem = downloadViewModel.createResultItemFromDownload(downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem
) {
lifecycleScope.launch {
val idx = downloads.indexOfFirst { it.id == item.id }
downloads[idx] = item
withContext(Dispatchers.Main){
ttle.text = "${index + 1}. ${item.title}"
}
}
}
}
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, onItemUpdated)
bottomSheet.show(supportFragmentManager, "configureDownloadSingleSheet")
}
if (historyItem != null){
ttle.setOnClickListener {
showHistoryItemDetailsCard(historyItem, context, isPresent = true,
removeItem = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
},
redownloadItem = { },
redownloadShowDownloadCard = {}
)
}
sheet.dismiss()
}
}
ttle.setOnLongClickListener {
showGenericDeleteDialog(context, ttle.text.toString(), accepted = {
linearLayout.removeView(alreadyExistsItem)
if (linearLayout.childCount == 0){
dialog?.dismiss()
}
})
true
}
linearLayout.addView(alreadyExistsItem)
}
}
errDialog.setView(linearLayout)
errDialog.setPositiveButton(downloadAnywayAction.first) { d:DialogInterface?, _:Int ->
CoroutineScope(Dispatchers.IO).launch {
linearLayout.children.forEach {view ->
val downloadItem = downloads.first { it.id == (view.tag as String).toLong()}
downloadItem.id = 0
downloadViewModel.queueDownloads(listOf(downloadItem), true)
}
}
d?.dismiss()
}
errDialog.setNegativeButton(R.string.schedule) { d:DialogInterface?, _:Int ->
sheet.findViewById<Button>(R.id.bottomsheet_schedule_button)?.apply {
setOnClickListener {
showDatePicker(supportFragmentManager) { calendar ->
CoroutineScope(Dispatchers.IO).launch {
val items = mutableListOf<DownloadItem>()
linearLayout.children.forEach {view ->
val downloadItem = downloads.first { it.id == (view.tag as String).toLong()}
downloadItem.downloadStartTime = calendar.timeInMillis
downloadItem.id = 0
items.add(downloadItem)
downloadViewModel.deleteProcessing()
val items = list.map { it.first }
items.forEach {
it.id = 0
it.downloadStartTime = calendar.timeInMillis
}
runBlocking {
val chunks = items.chunked(10)
for (c in chunks) {
downloadViewModel.queueDownloads(c, true)
}
downloadViewModel.queueDownloads(items, true)
val first = items.first()
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime)
withContext(Dispatchers.Main){
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
val date = SimpleDateFormat(
DateFormat.getBestDateTimePattern(
Locale.getDefault(),
"ddMMMyyyy - HHmm"
), Locale.getDefault()
).format(first.downloadStartTime)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.download_rescheduled_to) + " " + date,
Toast.LENGTH_LONG
).show()
}
}
withContext(Dispatchers.Main){
d?.dismiss()
withContext(Dispatchers.Main) {
sheet.dismiss()
}
}
}
}
dialog = errDialog.show()
}
sheet.setOnDismissListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
}
}
sheet.findViewById<BottomAppBar>(R.id.bottomAppBar)?.isVisible = false
sheet.show()
}
@ -1903,14 +1984,14 @@ object UiUtil {
builder.setView(view)
builder.setPositiveButton(
context.getString(R.string.dismiss)
context.getString(android.R.string.copy)
) { _: DialogInterface?, _: Int ->
copyToClipboard(txt, context)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(android.R.string.cancel)
context.getString(R.string.dismiss)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()

View file

@ -5,8 +5,8 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver
import com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers.AlarmStopReceiver
import java.util.Calendar
class AlarmScheduler(private val context: Context) {

View file

@ -29,6 +29,7 @@ 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.util.Extensions.getMediaDuration
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
@ -228,7 +229,7 @@ class DownloadWorker(
val file = File(this)
var duration = downloadItem.duration
val d = file.getMediaDuration(context)
if (d > 0) duration = infoUtil.formatIntegerDuration(d, Locale.US)
if (d > 0) duration = d.toStringDuration(Locale.US)
downloadItem.format.filesize = file.length()
downloadItem.format.container = file.extension

View file

@ -0,0 +1,255 @@
package com.deniscerri.ytdlnis.work
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver
import com.deniscerri.ytdlnis.util.DownloadUtil
import com.deniscerri.ytdlnis.util.Extensions.closestValue
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.Calendar
class ObserveSourceWorker(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val notificationUtil = NotificationUtil(App.instance)
val sourceID = inputData.getLong("id", 0)
if (sourceID == 0L) return Result.failure()
val dbManager = DBManager.getInstance(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao)
val historyRepo = HistoryRepository(dbManager.historyDao)
val downloadRepo = DownloadRepository(dbManager.downloadDao)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val infoUtil = InfoUtil(context)
val item = repo.getByID(sourceID)
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){
DownloadUtil.cancelObservationTaskByID(context, item.id)
return Result.failure()
}
val workerID = System.currentTimeMillis().toInt()
val notification = notificationUtil.createObserveSourcesNotification(item.name)
val foregroundInfo = ForegroundInfo(workerID, notification)
setForegroundAsync(foregroundInfo)
item.runCount = item.runCount + 1
var res = runCatching { infoUtil.getFromYTDL(item.url).toList() }.getOrElse { listOf() }
res = if (!item.retryMissingDownloads){
res.filter { result ->
//all items that are not present in history
!historyRepo.getAllByURL(result!!.url).filter { hi ->
hi.downloadPath.any { path -> FileUtil.exists(path) }
}.map { it.url }.contains(result.url)
}
}else{
//all items that are not already processed
res.filter { !item.alreadyProcessedLinks.contains(it!!.url) }
}
val items = mutableListOf<DownloadItem>()
res.forEach {
val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java)
val downloadItem = Gson().fromJson(string, DownloadItem::class.java)
downloadItem.url = it!!.url
downloadItem.id = 0L
items.add(downloadItem)
}
if (items.isNotEmpty()){
//QUEUE DOWNLOADS
//COPY OF QUEUE DOWNLOADS IN DOWNLOAD VIEW MODEL. NEEDS TO BE UPDATED IF THAT IS UPDATED
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = downloadRepo.getActiveAndQueuedDownloads()
val queuedItems = mutableListOf<DownloadItem>()
val existing = mutableListOf<DownloadItem>()
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
alarmScheduler.schedule()
}
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
}
runBlocking {
items.forEach {
if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString()
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null) {
it.id = existingDownload.id
existing.add(it)
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepo.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistory = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistory != null){
it.id = existingHistory.id
existing.add(it)
}else{
if (it.id == 0L){
it.id = downloadRepo.insert(it)
}else if (it.status == DownloadRepository.Status.Queued.toString()){
downloadRepo.update(it)
}
queuedItems.add(it)
}
}
}
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
DownloadUtil.startDownloadWorker(queuedItems, context)
if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
try{
updateDownloadItem(it, infoUtil, downloadRepo, dbManager.resultDao)
}catch (ignored: Exception){}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{
updateDownloadItem(it, infoUtil, downloadRepo, dbManager.resultDao)
}catch (ignored: Exception){}
}
}
}
item.alreadyProcessedLinks.addAll(items.map { it.url })
}
if (item.runCount > item.endsAfterCount && item.endsAfterCount > 0){
item.status = ObserveSourcesRepository.SourceStatus.STOPPED
withContext(Dispatchers.IO){
repo.update(item)
}
return Result.failure()
}
val currentTime = System.currentTimeMillis()
if (item.endsDate >= currentTime || item.endsAfterCount == item.runCount){
item.status = ObserveSourcesRepository.SourceStatus.STOPPED
withContext(Dispatchers.IO){
repo.update(item)
}
DownloadUtil.cancelObservationTaskByID(context, item.id)
return Result.success()
}
scheduleForNextTime(item)
return Result.success()
}
private fun scheduleForNextTime(item: ObserveSourcesItem){
val alarmManager = context.getSystemService(AlarmManager::class.java)
val intent = Intent(context, ObserveAlarmReceiver::class.java)
intent.putExtra("id", item.id)
val c = Calendar.getInstance()
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = item.everyTime
c.set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
c.set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
when(item.everyCategory){
ObserveSourcesRepository.EveryCategory.DAY -> {
c.add(Calendar.DAY_OF_MONTH, item.everyNr)
}
ObserveSourcesRepository.EveryCategory.WEEK -> {
if(item.everyWeekDay.isEmpty()){
c.add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}else{
val weekDayID = c.get(Calendar.DAY_OF_WEEK).toString()
val followingWeekDay = (item.everyWeekDay.firstOrNull { it.toInt() > weekDayID.toInt() } ?: item.everyWeekDay.minBy { it.toInt() }).toInt()
c.set(Calendar.DAY_OF_WEEK, followingWeekDay)
if (item.everyNr > 1){
c.add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}
}
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
c.add(Calendar.MONTH, item.everyNr)
c.set(Calendar.DAY_OF_MONTH, item.everyMonthDay)
}
}
alarmManager.setExact(
AlarmManager.RTC,
c.timeInMillis,
PendingIntent.getBroadcast(context, item.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
}
private fun updateDownloadItem(
downloadItem: DownloadItem,
infoUtil: InfoUtil,
repository: DownloadRepository,
resultDao: ResultDao
) : Boolean {
var wasQuickDownloaded = false
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
runCatching {
val info = infoUtil.getMissingInfo(downloadItem.url)
if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString()
if (downloadItem.author.isEmpty()) downloadItem.author = info?.author.toString()
downloadItem.duration = info?.duration.toString()
downloadItem.website = info?.website.toString()
if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info?.thumb.toString()
runBlocking {
wasQuickDownloaded = resultDao.getCountInt() == 0
repository.updateWithoutUpsert(downloadItem)
}
}
}
return wasQuickDownloaded
}
}

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.54,5.23l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6c-0.47,0 -0.88,0.21 -1.16,0.55L3.46,5.23C3.17,5.57 3,6.02 3,6.5V19c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5c0,-0.48 -0.17,-0.93 -0.46,-1.27zM12,17.5L6.5,12H10v-2h4v2h3.5L12,17.5zM5.12,5l0.81,-1h12l0.94,1H5.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="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z"/>
</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="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
</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="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>

View file

@ -0,0 +1,3 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="50dp" />
</shape>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!--
~ Any changes to the tint here needs to be mirrored in the call to
~ Utils.getGradientDrawableWithTintAttr() as well.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
android:tint="?colorControlActivated">
<corners android:radius="8dp" />
<padding android:top="4dp" android:bottom="4dp" />
<size android:width="8dp" android:height="52dp" />
<solid android:color="@android:color/white" />
</shape>

View file

@ -232,18 +232,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="5dp"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
android:src="@drawable/ic_clipboard"
android:text="@string/download_all"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -253,6 +254,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />

View file

@ -243,18 +243,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="5dp"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
android:src="@drawable/ic_clipboard"
android:text="@string/download_all"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -264,6 +265,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />

View file

@ -232,18 +232,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="5dp"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
android:src="@drawable/ic_clipboard"
android:text="@string/download_all"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -253,6 +254,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />

View file

@ -262,18 +262,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="5dp"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
android:src="@drawable/ic_clipboard"
android:text="@string/download_all"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -283,6 +284,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />

View file

@ -174,6 +174,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/save_auto_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/save_auto_subs"/>
<com.google.android.material.chip.Chip
android:id="@+id/subtitle_languages"
style="@style/Widget.Material3.Chip.Assist.Elevated"

View file

@ -1,9 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/existing_list"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/download_recyclerview"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
</LinearLayout>
</androidx.recyclerview.widget.RecyclerView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,7 +1,7 @@
<?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"
android:id="@+id/playlist_card_constraintLayout"
android:id="@+id/already_exists_constraint"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:checkable="true"
@ -23,22 +23,42 @@
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toStartOf="@+id/already_exists_edit"
app:layout_constraintEnd_toStartOf="@+id/already_exists_buttons"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/already_exists_edit"
style="@style/Widget.Material3.Button.IconButton.Filled"
<LinearLayout
android:id="@+id/already_exists_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_edit"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/already_exists_title" />
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/already_exists_edit"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_edit"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/already_exists_title"
app:layout_constraintVertical_bias="1.0" />
<Button
android:id="@+id/already_exists_delete"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_delete_24"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/already_exists_title" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -43,20 +43,24 @@
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/download"
android:textSize="25sp"
android:maxLines="2"
android:singleLine="false"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/configure_download"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />

View file

@ -45,6 +45,29 @@
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/duration"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/filesize"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"

View file

@ -298,6 +298,41 @@
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bitrate_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:weightSum="100"
android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" >
<TextView
android:id="@+id/bitrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:layout_constraintHorizontal_bias="0.0"
android:text="@string/bitrate"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bitrate_value"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bitrate_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="end"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -116,6 +116,7 @@
android:textColor="@color/white"
android:textStyle="bold"
app:cornerRadius="10dp"
android:maxLength="10"
app:drawableStartCompat="@drawable/ic_music"
android:textCursorDrawable="@string/audio_format"
app:drawableTint="@color/white"
@ -178,6 +179,7 @@
android:clickable="false"
android:gravity="bottom|end"
android:maxLength="10"
android:ellipsize="end"
app:cornerRadius="10dp"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -64,7 +64,6 @@
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:icon="@drawable/baseline_align_horizontal_left_24"
app:layout_constraintBottom_toBottomOf="@id/format_refresh"
app:layout_constraintEnd_toStartOf="@+id/format_refresh"
@ -77,6 +76,7 @@
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/update_formats"
android:layout_marginStart="10dp"
app:icon="@drawable/ic_refresh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -88,6 +88,7 @@
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"

View file

@ -57,6 +57,16 @@
<View style="@style/Divider.Vertical"/>
<com.google.android.material.chip.Chip
android:id="@+id/not_deleted_chip"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/not_deleted"/>
<View android:id="@+id/notdeleted_divider" style="@style/Divider.Vertical"/>
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"

View file

@ -161,18 +161,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="5dp"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
android:src="@drawable/ic_clipboard"
android:text="@string/download_all"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -181,8 +182,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
@ -193,8 +194,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>

View file

@ -100,6 +100,21 @@
android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/baseline_cookie_24" />
<TextView
android:id="@+id/observe_sources"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="35dp"
android:textSize="16sp"
android:paddingVertical="15dp"
android:text="@string/observe_sources"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/baseline_calendar_month_24" />
<TextView
android:id="@+id/terminate"
android:layout_width="match_parent"

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
tools:context="com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:elevation="0dp"
app:title="@string/observe_sources"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back"
app:menu="@menu/observe_sources_menu"
android:layout_height="match_parent"/>
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<com.google.android.material.chip.Chip
android:id="@+id/newSource"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:text="@string/new_source"
app:chipIcon="@drawable/ic_plus" />
<androidx.recyclerview.widget.RecyclerView
android:layout_below="@+id/newSource"
android:id="@+id/source_recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
>
</androidx.recyclerview.widget.RecyclerView>
</RelativeLayout>
<include layout="@layout/no_results"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,476 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="wrap_content"
android:fillViewport="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="2"
android:singleLine="false"
android:text="@string/observe_sources"
android:textSize="25sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/configure_download"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/okButton"
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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/title_textinput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingHorizontal="10dp"
android:paddingBottom="10dp"
android:hint="@string/title"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/url_textinput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingBottom="10dp"
android:hint="@string/url"
app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_clipboard"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/download_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginBottom="10dp"
android:backgroundTint="@android:color/transparent"
app:tabGravity="start"
app:tabMinWidth="0dp"
app:tabMode="scrollable">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/audio" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/video" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/command" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/download_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout android:layout_width="match_parent"
android:id="@+id/adjust"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:paddingBottom="15dp"
android:paddingHorizontal="20dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/every" />
<LinearLayout
android:layout_width="match_parent"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/every_textinput"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="number"
android:textAlignment="center"
android:text="1"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_weight="45"
android:paddingStart="10dp"
android:hint="@string/every"
android:layout_height="match_parent">
<AutoCompleteTextView
android:id="@+id/everyCat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="none"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/everyMonthDay_textInput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:paddingStart="10dp"
android:layout_weight="25"
android:hint="@string/day"
android:layout_height="wrap_content">
<AutoCompleteTextView
android:id="@+id/everyMonthDay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="none"
app:simpleItems="@array/monthDays"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/weekdays"
android:layout_width="match_parent"
app:chipSpacingVertical="0dp"
android:paddingVertical="5dp"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/m"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"/>
<com.google.android.material.chip.Chip
android:id="@+id/t"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"/>
<com.google.android.material.chip.Chip
android:id="@+id/w"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"/>
<com.google.android.material.chip.Chip
android:id="@+id/th"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"/>
<com.google.android.material.chip.Chip
android:id="@+id/f"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"/>
<com.google.android.material.chip.Chip
android:id="@+id/sa"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"/>
<com.google.android.material.chip.Chip
android:id="@+id/su"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"/>
</com.google.android.material.chip.ChipGroup>
<LinearLayout
android:layout_width="match_parent"
android:paddingHorizontal="10dp"
android:paddingTop="10dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/everyday_textInput"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingEnd="10dp"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="number"
android:textAlignment="center"
android:padding="15dp"
android:text="1"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/every_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:hint="@string/set_time"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:paddingBottom="15dp"
android:paddingHorizontal="20dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/starts" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/start_time"
android:paddingHorizontal="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:hint="@string/starts"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/startsMonth_textInput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_weight="45"
android:paddingHorizontal="10dp"
android:paddingBottom="10dp"
android:layout_height="0dp">
<AutoCompleteTextView
android:id="@+id/startsMonth"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp"
android:inputType="none"
/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:paddingHorizontal="20dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/ends" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingBottom="20dp"
android:orientation="vertical">
<RadioButton
android:id="@+id/never"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:checked="true"
android:text="@string/never" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/on"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/on"
android:paddingHorizontal="10dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/on_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintEnabled="false"
android:layout_marginStart="15dp"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:padding="15dp"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/after"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/after"
android:paddingHorizontal="10dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/after_nr"
android:layout_width="wrap_content"
android:layout_height="match_parent"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="number"
android:textAlignment="center"
android:text="1"
android:padding="15dp"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:text="@string/occurrences"/>
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/retry_missing_downloads"
android:layout_width="match_parent"
android:layout_marginTop="20dp"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/retry_missing_downloads"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,2:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/observe_sources_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="20dp"
app:cardElevation="5dp"
app:strokeColor="?attr/colorPrimary"
app:checkedIconTint="?attr/colorPrimary"
app:cardMaxElevation="12dp"
app:cardPreventCornerOverlap="true"
android:checkable="true"
app:strokeWidth="0dp"
android:layout_margin="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/result_image_view"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/download_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:indeterminate="true"
android:visibility="gone"
android:alpha="0.7"
android:scaleY="100" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:dividerPadding="5dp"
android:orientation="vertical"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toTopOf="@+id/search"
app:layout_constraintEnd_toStartOf="@id/check_missing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textColor="#FFF"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="bottom"
android:maxLines="1"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="1.5"
android:textColor="#FFF"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<Button
android:id="@+id/check_missing"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
app:layout_constraintVertical_bias="0.0"
android:padding="0dp"
android:minHeight="0dp"
app:icon="@drawable/ic_restore"
app:layout_constraintBottom_toTopOf="@+id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/info"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_margin="10dp"
app:chipMinTouchTargetSize="0dp"
app:chipIcon="@drawable/baseline_loop_24"
app:chipIconVisible="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/search"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_margin="10dp"
app:elevation="0dp"
android:elevation="0dp"
app:borderWidth="0dp"
android:src="@drawable/ic_search"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -23,6 +23,7 @@
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:drawablePadding="20dp"
android:maxLength="100"
android:ellipsize="end"
android:focusable="true"
android:gravity="center_vertical"

View file

@ -27,4 +27,10 @@
android:icon="@drawable/ic_delete_all"
android:title="@string/clear_saved"
app:showAsAction="never" />
<item
android:id="@+id/copy_urls"
android:icon="@drawable/ic_delete_all"
android:title="@string/copy_urls"
app:showAsAction="never" />
</menu>

View file

@ -29,6 +29,10 @@
android:icon="@drawable/baseline_cookie_24"
android:checkable="true"
android:id="@+id/cookiesFragment"/>
<item android:title="@string/observe_sources"
android:icon="@drawable/baseline_calendar_month_24"
android:checkable="true"
android:id="@+id/observeSourcesFragment"/>
<item android:title="@string/kill_app"
android:checkable="true"
android:icon="@drawable/ic_power"

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">
<item
android:id="@+id/delete_sources"
android:title="@string/delete_all_sources"
app:showAsAction="never" />
</menu>

View file

@ -69,6 +69,9 @@
<action
android:id="@+id/action_moreFragment_to_terminalActivity"
app:destination="@id/terminalActivity" />
<action
android:id="@+id/action_moreFragment_to_observeSourcesFragment"
app:destination="@id/observeSourcesFragment" />
</fragment>
<activity
android:id="@+id/terminalActivity"
@ -163,9 +166,29 @@
<dialog
android:id="@+id/downloadMultipleBottomSheetDialog2"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog"
android:label="DownloadMultipleBottomSheetDialog" />
android:label="DownloadMultipleBottomSheetDialog" >
<action
android:id="@+id/action_downloadMultipleBottomSheetDialog2_to_configureDownloadBottomSheetDialog"
app:destination="@id/configureDownloadBottomSheetDialog" />
</dialog>
<dialog
android:id="@+id/cutVideoBottomSheetDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.CutVideoBottomSheetDialog"
android:label="CutVideoBottomSheetDialog" />
<dialog
android:id="@+id/configureDownloadBottomSheetDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog"
android:label="ConfigureDownloadBottomSheetDialog" />
<fragment
android:id="@+id/observeSourcesFragment"
android:name="com.deniscerri.ytdlnis.ui.more.ObserveSourcesFragment"
android:label="ObserveSourcesFragment" >
<action
android:id="@+id/action_observeSourcesFragment_to_observeSourcesBottomSheetDialog"
app:destination="@id/observeSourcesBottomSheetDialog" />
</fragment>
<dialog
android:id="@+id/observeSourcesBottomSheetDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ObserveSourcesBottomSheetDialog"
android:label="ObserveSourcesBottomSheetDialog" />
</navigation>

View file

@ -37,7 +37,7 @@
<string name="sponsorblock_intro">ئینترۆ</string>
<string name="sponsorblock_fillers">فیلتەرەکان</string>
<string name="sponsorblock_reminders">بیرخەرەوەکانی بەشداربوون</string>
<string name="cancel_download">پاشگەزبوونەوە</string>
<string name="cancel_task">پاشگەزبوونەوە</string>
<string name="logs">تۆمارەکان</string>
<string name="audio_quality">کوالێتی دەنگ</string>
<string name="audio_format">فۆرماتی دەنگ</string>

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