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, "formatVersion": 1,
"database": { "database": {
"version": 14, "version": 14,
"identityHash": "5cea521e1a26473b2ec5298791f10ac4", "identityHash": "5be3b7ae26c7716f2192470a3a9249a8",
"entities": [ "entities": [
{ {
"tableName": "results", "tableName": "results",
@ -587,12 +587,131 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "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": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelWorkReceiver" /> <receiver android:name=".receiver.CancelWorkReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" /> <receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.AlarmStartReceiver" /> <receiver android:name=".receiver.downloadAlarmReceivers.AlarmStartReceiver" />
<receiver android:name=".receiver.AlarmStopReceiver" /> <receiver android:name=".receiver.downloadAlarmReceivers.AlarmStopReceiver" />
<receiver android:name=".receiver.ObserveAlarmReceiver" />
<activity <activity
android:name=".receiver.ResumeActivity" android:name=".receiver.ResumeActivity"

View file

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

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.database
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.deniscerri.ytdlnis.database.models.AudioPreferences import com.deniscerri.ytdlnis.database.models.AudioPreferences
import com.deniscerri.ytdlnis.database.models.ChapterItem import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.VideoPreferences import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@ -57,4 +58,19 @@ class Converters {
val gson = Gson() val gson = Gson()
return gson.toJson(list) 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, TemplateShortcut::class,
CookieItem::class, CookieItem::class,
LogItem::class, LogItem::class,
TerminalItem::class TerminalItem::class,
ObserveSourcesItem::class
], ],
version = 14, version = 14,
autoMigrations = [ autoMigrations = [
@ -37,7 +38,7 @@ import com.deniscerri.ytdlnis.database.models.*
AutoMigration (from = 10, to = 11), AutoMigration (from = 10, to = 11),
AutoMigration (from = 11, to = 12), AutoMigration (from = 11, to = 12),
AutoMigration (from = 12, to = 13), AutoMigration (from = 12, to = 13),
//AutoMigration (from = 13, to = 14), MANUALLY HANDLED // AutoMigration (from = 13, to = 14) MANUALLY HANDLED
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){
@ -49,6 +50,7 @@ abstract class DBManager : RoomDatabase(){
abstract val cookieDao: CookieDao abstract val cookieDao: CookieDao
abstract val logDao: LogDao abstract val logDao: LogDao
abstract val terminalDao: TerminalDao abstract val terminalDao: TerminalDao
abstract val observeSourcesDao: ObserveSourcesDao
enum class SORTING{ enum class SORTING{
DESC, ASC DESC, ASC

View file

@ -1,39 +1,7 @@
package com.deniscerri.ytdlnis.database package com.deniscerri.ytdlnis.database
import android.annotation.SuppressLint 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.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 { object Migrations {
@ -48,7 +16,10 @@ object Migrations {
val path = cursor.getString(cursor.getColumnIndex("downloadPath")) val path = cursor.getString(cursor.getColumnIndex("downloadPath"))
val newPath = "[\"${path.replace("\"", "\\\"").replace("'", "''")}\"]" val newPath = "[\"${path.replace("\"", "\\\"").replace("'", "''")}\"]"
database.execSQL("UPDATE history SET downloadPath = '${newPath}' WHERE id = $id") 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") @Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookies() : List<CookieItem> 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") @Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookiesFlow() : Flow<List<CookieItem>> fun getAllCookiesFlow() : Flow<List<CookieItem>>

View file

@ -16,6 +16,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')") @Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')")
fun getActiveDownloads() : Flow<List<DownloadItem>> 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')") @Query("SELECT COUNT(*) FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')")
fun getActiveDownloadsCountFlow() : Flow<Int> fun getActiveDownloadsCountFlow() : Flow<Int>
@ -25,9 +28,29 @@ interface DownloadDao {
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)") @Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatus(status : List<String>) : Int 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')") @Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused','PausedReQueued')")
fun getActiveAndPausedDownloadsList() : List<DownloadItem> fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem> fun getActiveDownloadsList() : List<DownloadItem>
@ -126,6 +149,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Saved'") @Query("DELETE FROM downloads WHERE status='Saved'")
suspend fun deleteSaved() suspend fun deleteSaved()
@Query("DELETE FROM downloads WHERE status='Processing'")
suspend fun deleteProcessing()
@Query("DELETE FROM downloads WHERE id in (:list)") @Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>) 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)") @Query("Select id from downloads where id not in (:list) and status in (:status)")
fun getDownloadIDsNotPresentInList(list: List<Long>, status: List<String>) : List<Long> 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)") @Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)")
fun resetScheduleTimeForItems(list: List<Long>) fun resetScheduleTimeForItems(list: List<Long>)
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
fun reQueueDownloadItems(list: List<Long>) fun reQueueDownloadItems(list: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing'")
fun updateProcessingtoSavedStatus()
@Transaction @Transaction
fun putAtTopOfTheQueue(id: Long){ fun putAtTopOfTheQueue(id: Long){
val downloads = getQueuedDownloadsList() 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") @SerializedName(value = "url")
var url: String? = "", var url: String? = "",
@SerializedName(value = "language", alternate = ["audioTrackLocale"]) @SerializedName(value = "language", alternate = ["audioTrackLocale"])
val lang: String? = "" val lang: String? = "",
@SerializedName(value = "tbr", alternate = ["bitrate"])
var tbr: String? = ""
) : Parcelable ) : 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 splitByChapters: Boolean = false,
var sponsorBlockFilters: ArrayList<String> = arrayListOf(), var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false, var writeSubs: Boolean = false,
var writeAutoSubs: Boolean = false,
var subsLanguages: String = "en.*,.*-orig", var subsLanguages: String = "en.*,.*-orig",
var audioFormatIDs : ArrayList<String> = arrayListOf(), var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false var removeAudio: Boolean = false

View file

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

View file

@ -18,6 +18,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getAllDownloads()} pagingSourceFactory = {downloadDao.getAllDownloads()}
) )
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged() val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager( val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getQueuedDownloads()} pagingSourceFactory = {downloadDao.getQueuedDownloads()}
@ -38,7 +39,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCountFlow() val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCountFlow()
enum class Status { 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 { suspend fun insert(item: DownloadItem) : Long {
@ -58,7 +59,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
private fun deleteCache(items: List<DownloadItem>) { private fun deleteCache(items: List<DownloadItem>) {
val cacheDir = FileUtil.getCachePath(App.instance) val cacheDir = FileUtil.getCachePath(App.instance)
items.forEach { 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() return downloadDao.getActiveAndPausedDownloadsList()
} }
fun getProcessingDownloads() : List<DownloadItem> {
return downloadDao.getProcessingDownloadsList()
}
fun getActiveAndQueuedDownloads() : List<DownloadItem> { fun getActiveAndQueuedDownloads() : List<DownloadItem> {
return downloadDao.getActiveAndQueuedDownloadsList() return downloadDao.getActiveAndQueuedDownloadsList()
} }
@ -124,6 +129,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.deleteSaved() downloadDao.deleteSaved()
} }
suspend fun deleteProcessing(){
downloadDao.deleteProcessing()
}
suspend fun deleteAllWithIDs(ids: List<Long>){ suspend fun deleteAllWithIDs(ids: List<Long>){
downloadDao.deleteAllWithIDs(ids) downloadDao.deleteAllWithIDs(ids)

View file

@ -26,8 +26,8 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getAllHistoryByURL(url) return historyDao.getAllHistoryByURL(url)
} }
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) : List<HistoryItem> { fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List<HistoryItem> {
return when(sortType){ var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString()) HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString()) HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(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){ 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() val items : ArrayList<ResultItem?> = arrayListOf()
do { do {
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0]?.playlistTitle ?: "" else "") 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) items.addAll(tmp.videos)
val tmpToken = tmp.nextPageToken val tmpToken = tmp.nextPageToken
if (tmpToken.isEmpty()) break if (tmpToken.isEmpty()) break
@ -72,10 +76,6 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
nextPageToken = tmpToken nextPageToken = tmpToken
} while (true) } while (true)
itemCount.value = items.size itemCount.value = items.size
val ids = resultDao.insertMultiple(items.toList())
ids.forEachIndexed { index, id ->
items[index]?.id = id
}
return items return items
} }

View file

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

View file

@ -10,6 +10,7 @@ import android.net.ConnectivityManager
import android.os.Looper import android.os.Looper
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.core.updateTransition
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
@ -17,6 +18,7 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder 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.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository 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.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.deniscerri.ytdlnis.work.DownloadWorker import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -57,7 +62,7 @@ import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class DownloadViewModel(application: Application) : AndroidViewModel(application) { class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager private val dbManager: DBManager
val repository : DownloadRepository val repository : DownloadRepository
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
@ -66,6 +71,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val allDownloads : Flow<PagingData<DownloadItem>> val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>> val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>> val activeDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItem>>
val activeDownloadsCount : Flow<Int> val activeDownloadsCount : Flow<Int>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>> val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>> val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
@ -89,19 +95,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private val dao: DownloadDao private val dao: DownloadDao
private val historyRepository: HistoryRepository private val historyRepository: HistoryRepository
data class DownloadsUiState( data class AlreadyExistsUIState(
var errorMessage: Pair<Int, Int>?, var historyItems: MutableList<Long>,
//action title, type, data var downloadItems : MutableList<Long>
var actions: MutableList<Triple<Int, DownloadsAction, List<DownloadItem>?>>?
) )
enum class DownloadsAction { val alreadyExistsUiState: MutableStateFlow<AlreadyExistsUIState> = MutableStateFlow(AlreadyExistsUIState(
DOWNLOAD_ANYWAY historyItems = mutableListOf(),
} downloadItems = mutableListOf()
val uiState: MutableStateFlow<DownloadsUiState> = MutableStateFlow(DownloadsUiState(
errorMessage = null,
actions = null
)) ))
enum class Type { enum class Type {
@ -126,6 +127,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
allDownloads = repository.allDownloads.flow allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads activeDownloads = repository.activeDownloads
processingDownloads = repository.processingDownloads
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount
savedDownloads = repository.savedDownloads.flow savedDownloads = repository.savedDownloads.flow
cancelledDownloads = repository.cancelledDownloads.flow cancelledDownloads = repository.cancelledDownloads.flow
@ -223,6 +225,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false) val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_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 addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false) val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
@ -261,6 +264,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
addChapters, false, addChapters, false,
ArrayList(sponsorblock), ArrayList(sponsorblock),
saveSubs, saveSubs,
saveAutoSubs,
audioFormatIDs = preferredAudioFormats audioFormatIDs = preferredAudioFormats
) )
@ -391,6 +395,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
fun createDownloadItemFromHistory(historyItem: HistoryItem) : DownloadItem { fun createDownloadItemFromHistory(historyItem: HistoryItem) : DownloadItem {
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false) val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_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 addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false) val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_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 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 var path = defaultPath
historyItem.downloadPath.first().apply { historyItem.downloadPath.first().apply {
File(this).parent?.apply { File(this).parent?.apply {
@ -480,7 +485,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.audio -> { Type.audio -> {
return cloneFormat ( return cloneFormat (
try { 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() val requirements = getPreferredAudioRequirements()
theFormats.maxByOrNull { f -> requirements.count{req -> req(f)} } ?: throw Exception() theFormats.maxByOrNull { f -> requirements.count{req -> req(f)} } ?: throw Exception()
}catch (e: Exception){ }catch (e: Exception){
@ -492,7 +497,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.video -> { Type.video -> {
return cloneFormat( return cloneFormat(
try { 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) { when (videoQualityPreference) {
"worst" -> { "worst" -> {
theFormats.last() 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) { fun deleteCancelled() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteCancelled() repository.deleteCancelled()
} }
@ -599,6 +612,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
repository.deleteSaved() repository.deleteSaved()
} }
fun deleteProcessing() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
}
fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){ fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){
repository.deleteAllWithIDs(ids) repository.deleteAllWithIDs(ids)
} }
@ -646,27 +663,31 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch { suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items) dbManager.downloadDao.resetScheduleTimeForItems(items)
startDownloadWorker(emptyList()) DownloadUtil.startDownloadWorker(emptyList(), application)
} }
suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{ suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{
dbManager.downloadDao.putAtTopOfTheQueue(id) dbManager.downloadDao.putAtTopOfTheQueue(id)
startDownloadWorker(emptyList()) DownloadUtil.startDownloadWorker(emptyList(), application)
} }
suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch { suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.reQueueDownloadItems(items) 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 context = App.instance
val alarmScheduler = AlarmScheduler(context) val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = withContext(Dispatchers.IO){ val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads() repository.getActiveAndQueuedDownloads()
} }
val queuedItems = mutableListOf<DownloadItem>() 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 //if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
@ -680,122 +701,85 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
items.forEach { items.forEach {
if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString() if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString()
val currentCommand = infoUtil.buildYoutubeDLRequest(it) var alreadyExists = false
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> if(!ignoreDuplicate){
d.id = 0 val currentCommand = infoUtil.buildYoutubeDLRequest(it)
d.logID = null val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
d.customFileNameTemplate = it.customFileNameTemplate val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.status = DownloadRepository.Status.Queued.toString() d.id = 0
d.toString() == it.toString() d.logID = null
} d.customFileNameTemplate = it.customFileNameTemplate
if (existingDownload != null && !ignoreDuplicate) { d.status = DownloadRepository.Status.Queued.toString()
it.id = existingDownload.id d.toString() == it.toString()
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) } }
} }
val existingHistory = history.firstOrNull { if (existingDownload != null){
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") it.status = DownloadRepository.Status.Saved.toString()
} val id = runBlocking {
repository.insert(it)
if (existingHistory != null && !ignoreDuplicate){ }
it.id = existingHistory.id alreadyExists = true
existing.add(it) existingDownloads.add(id)
}else{ }else{
if (it.id == 0L){ //check if downloaded and file exists
val id = runBlocking { val history = withContext(Dispatchers.IO){
repository.insert(it) historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
it.id = id
}else if (it.status == DownloadRepository.Status.Queued.toString()){
withContext(Dispatchers.IO){
repository.update(it)
}
} }
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()){ return Pair(existingDownloads, existingHistory)
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()
}
}
} }
fun getQueuedCollectedFileSize() : Long { 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() }) 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( private fun updateDownloadItem(
downloadItem: DownloadItem, downloadItem: DownloadItem,
infoUtil: InfoUtil, infoUtil: InfoUtil,

View file

@ -22,6 +22,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
val websiteFilter = MutableLiveData("") val websiteFilter = MutableLiveData("")
private val queryFilter = MutableLiveData("") private val queryFilter = MutableLiveData("")
private val formatFilter = MutableLiveData("") private val formatFilter = MutableLiveData("")
private val notDeletedFilter = MutableLiveData(false)
val allItems : LiveData<List<HistoryItem>> val allItems : LiveData<List<HistoryItem>>
private var _items = MediatorLiveData<List<HistoryItem>>() private var _items = MediatorLiveData<List<HistoryItem>>()
@ -32,19 +33,23 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
allItems = repository.items.asLiveData() allItems = repository.items.asLiveData()
_items.addSource(allItems){ _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){ _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){ _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){ _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){ _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 formatFilter.value = filter
} }
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) = viewModelScope.launch(Dispatchers.IO){ fun setNotDeleted(filter: Boolean){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort)) 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> { 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)) else -> res.addAll(repository.getDefault(inputQuery, resetResults))
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e !is kotlinx.coroutines.CancellationException){ if (updateResultDataJob?.isCancelled == false){
uiState.update {it.copy( uiState.update {it.copy(
processing = false, processing = false,
errorMessage = Pair(R.string.no_results, e.message.toString()), 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 package com.deniscerri.ytdlnis.receiver
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@ -97,6 +98,7 @@ class ShareActivity : BaseActivity() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
navController = navHostFragment.findNavController() navController = navHostFragment.findNavController()
navController.addOnDestinationChangedListener(object: NavController.OnDestinationChangedListener{ navController.addOnDestinationChangedListener(object: NavController.OnDestinationChangedListener{
@SuppressLint("RestrictedApi")
override fun onDestinationChanged( override fun onDestinationChanged(
controller: NavController, controller: NavController,
destination: NavDestination, 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.BroadcastReceiver
import android.content.Context import android.content.Context
@ -13,7 +13,6 @@ import java.util.concurrent.TimeUnit
class AlarmStartReceiver : BroadcastReceiver() { class AlarmStartReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) { override fun onReceive(p0: Context?, p1: Intent?) {
Log.e("assa", "Start")
val workConstraints = Constraints.Builder() val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download") .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.BroadcastReceiver
import android.content.Context import android.content.Context

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui package com.deniscerri.ytdlnis.ui
import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
@ -14,6 +15,7 @@ import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.view.* import android.view.*
import android.view.View.* import android.view.View.*
import android.view.animation.Interpolator
import android.widget.* import android.widget.*
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -35,14 +37,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R 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.ResultItem
import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem
import com.deniscerri.ytdlnis.database.models.SearchSuggestionType import com.deniscerri.ytdlnis.database.models.SearchSuggestionType
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter
import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter 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.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil 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.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -76,13 +79,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var downloadSelectedFab: ExtendedFloatingActionButton? = null private var downloadSelectedFab: ExtendedFloatingActionButton? = null
private var downloadAllFab: ExtendedFloatingActionButton? = null private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: FloatingActionButton? = null private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: CoordinatorLayout? = null private var homeFabs: CoordinatorLayout? = null
private var infoUtil: InfoUtil? = null private var infoUtil: InfoUtil? = null
private var downloadQueue: ArrayList<ResultItem>? = null private var downloadQueue: ArrayList<ResultItem>? = null
private lateinit var resultViewModel : ResultViewModel private lateinit var resultViewModel : ResultViewModel
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var historyViewModel : HistoryViewModel
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
@ -126,7 +130,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
uiHandler = Handler(Looper.getMainLooper()) uiHandler = Handler(Looper.getMainLooper())
selectedObjects = ArrayList() selectedObjects = ArrayList()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadQueue = ArrayList() downloadQueue = ArrayList()
resultsList = mutableListOf() resultsList = mutableListOf()
@ -167,7 +172,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchSuggestionsRecyclerView = view.findViewById(R.id.search_suggestions_recycler) searchSuggestionsRecyclerView = view.findViewById(R.id.search_suggestions_recycler)
searchSuggestionsRecyclerView?.layoutManager = LinearLayoutManager(context) searchSuggestionsRecyclerView?.layoutManager = LinearLayoutManager(context)
searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter
searchSuggestionsRecyclerView?.enableFastScroll() searchSuggestionsRecyclerView?.itemAnimator = null
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] 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() { override fun onResume() {
@ -278,15 +307,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){ if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){
arguments?.remove("showDownloadsWithUpdatedFormats") arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") val ids = arguments?.getLongArray("downloadIds") ?: return@launch
val items = mutableListOf<DownloadItem>() val type = downloadViewModel.updateItemsWithIdsToProcessingStatus(ids.toList())
ids?.forEach {
items.add(downloadViewModel.getItemByID(it))
downloadViewModel.deleteDownload(it)
}
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( 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 clipboardFab?.isVisible = false
initSearch(searchView!!) initSearch(searchView!!)
} }
lifecycleScope.launch {
clipboardFab?.extend()
delay(1000)
clipboardFab?.shrink()
}
} }
} }
} }
@ -679,11 +710,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
downloadViewModel.turnResultItemsToDownloadItems(resultsList!!) downloadViewModel.turnResultItemsToDownloadItems(resultsList!!)
} }
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( CoroutineScope(Dispatchers.IO).launch {
Pair("downloads", downloadList) downloadViewModel.insertToProcessing(downloadList)
)) }
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(Pair("type", downloadList[0].type)))
} else { } 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)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("downloads", downloadList) Pair("type", downloadList[0].type)
)) ))
} else { } else {
downloadViewModel.queueDownloads(downloadList) downloadViewModel.queueDownloads(downloadList)
@ -827,8 +866,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
chip.text = text chip.text = text
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.setOnClickListener { chip.setOnClickListener {
if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE
queriesChipGroup!!.removeView(chip) queriesChipGroup!!.removeView(chip)
if (queriesChipGroup!!.childCount == 0) queriesChipGroup!!.visibility = View.GONE
} }
queriesChipGroup!!.addView(chip) 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.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
resultViewModel.removeSearchQueryFromHistory(text) resultViewModel.removeSearchQueryFromHistory(text)
searchSuggestionsAdapter?.notifyItemRemoved(position) updateSearchViewItems(searchView!!.editText.text)
} }
deleteDialog.show() 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.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.dp import com.deniscerri.ytdlnis.util.Extensions.dp
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -61,6 +63,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
card.popup()
card.tag = "${item!!.id}##card" card.tag = "${item!!.id}##card"
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.image_view) 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.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -56,6 +57,8 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
card.popup()
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.image_view) 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.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
@ -54,13 +55,16 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
card.popup()
if (item == null) return
card.tag = item.id.toString()
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view) val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){ if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
val imageURL = item?.thumb val imageURL = item.thumb
if (!imageURL.isNullOrBlank()) { if (!imageURL.isNullOrBlank()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else { } else {
@ -72,7 +76,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
} }
val duration = card.findViewById<TextView>(R.id.duration) val duration = card.findViewById<TextView>(R.id.duration)
duration.text = item!!.duration duration.text = item.duration
// TITLE ---------------------------------- // TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title) val itemTitle = card.findViewById<TextView>(R.id.title)
@ -120,7 +124,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
if (btn.hasOnClickListeners()) btn.setOnClickListener(null) if (btn.hasOnClickListeners()) btn.setOnClickListener(null)
btn.setOnClickListener { btn.setOnClickListener {
onItemClickListener.onButtonClick(item.url) onItemClickListener.onButtonClick(item.id)
} }
when(item.type) { when(item.type) {
@ -136,18 +140,18 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
} }
card.setOnClickListener { card.setOnClickListener {
onItemClickListener.onCardClick(item.url) onItemClickListener.onCardClick(item.id)
} }
card.setOnLongClickListener { card.setOnLongClickListener {
onItemClickListener.onDelete(item.url); true onItemClickListener.onDelete(item.id); true
} }
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onButtonClick(itemURL: String) fun onButtonClick(id: Long)
fun onCardClick(itemURL: String) fun onCardClick(id: Long)
fun onDelete(itemURL: String) fun onDelete(id: Long)
} }
companion object { companion object {
@ -157,7 +161,10 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
} }
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { 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 androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<CookieItem?, CookieAdapter.ViewHolder>(AsyncDifferConfig.Builder( 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.item val card = holder.item
card.popup()
val title = card.findViewById<TextView>(R.id.title) val title = card.findViewById<TextView>(R.id.title)
title.text = item?.url title.text = item?.url

View file

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -50,6 +51,7 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.item val card = holder.item
card.popup()
val title = card.findViewById<TextView>(R.id.title) val title = card.findViewById<TextView>(R.id.title)
title.text = item?.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.R
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -55,6 +56,8 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
card.popup()
if (item == null) return if (item == null) return
card.tag = item.id.toString() card.tag = item.id.toString()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.ViewModelProvider 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.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,7 +43,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
private lateinit var fragmentAdapter : DownloadFragmentAdapter private lateinit var fragmentAdapter : DownloadFragmentAdapter
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var commandTemplateDao: CommandTemplateDao private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener
private lateinit var sharedPreferences : SharedPreferences private lateinit var sharedPreferences : SharedPreferences
@ -49,7 +52,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
onDownloadItemUpdateListener = listener onDownloadItemUpdateListener = listener
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
} }
@ -84,7 +87,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
var commandTemplateNr = 0 var commandTemplateNr = 0
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateDao.getTotalNumber() commandTemplateNr = commandTemplateViewModel.getTotalNumber()
if(commandTemplateNr <= 0){ if(commandTemplateNr <= 0){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f (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?) { override fun onTabSelected(tab: TabLayout.Tab?) {
if (tab!!.position == 2 && commandTemplateNr == 0){ if (tab!!.position == 2 && commandTemplateNr == 0){
tabLayout.selectTab(tabLayout.getTabAt(1)) 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){ }else if (tab.position == 1 && isAudioOnly){
tabLayout.selectTab(tabLayout.getTabAt(0)) tabLayout.selectTab(tabLayout.getTabAt(0))
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() 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){ return when(selectedTabPosition){
0 -> { 0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem f.downloadItem.apply { id = currentDownloadItem.id }
} }
1 -> { 1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.downloadItem f.downloadItem.apply { id = currentDownloadItem.id }
} }
else -> { else -> {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment 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( val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1 if (viewPager2.currentItem == 1) 0 else 1
) )
fragmentAdapter.setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
result.title = prevDownloadItem.title
result.author = prevDownloadItem.author
currentDownloadItem.title = prevDownloadItem.title
currentDownloadItem.author = prevDownloadItem.author
when(viewPager2.currentItem){ when(viewPager2.currentItem){
0 -> { 0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment kotlin.runCatching {
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
} }
1 -> { 1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment kotlin.runCatching {
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateSelectedAudioFormat(getDownloadItem(0).format) 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 -> {} 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.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.deniscerri.ytdlnis.util.VideoPlayerUtil import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -203,7 +204,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
//poll video progress //poll video progress
lifecycleScope.launch { lifecycleScope.launch {
videoProgress(player).collect { p -> videoProgress(player).collect { p ->
val currentTime = infoUtil.formatIntegerDuration(p, Locale.US) val currentTime = p.toStringDuration(Locale.US)
durationText.text = "$currentTime / ${item.duration}" durationText.text = "$currentTime / ${item.duration}"
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
if (toTextInput.editText!!.text.isNotBlank()){ if (toTextInput.editText!!.text.isNotBlank()){
@ -310,11 +311,11 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
val endValue = (endSeconds.toFloat() / endTimestamp) * 100 val endValue = (endSeconds.toFloat() / endTimestamp) * 100
if (seconds == 0) { if (seconds == 0) {
fromTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(startTimestamp, Locale.US)) fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else if (startValue > 100){ }else if (startValue > 100){
startTimestamp = 0 startTimestamp = 0
seconds = 0 seconds = 0
fromTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(startTimestamp, Locale.US)) fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
startValue = 0F startValue = 0F
} }
@ -350,14 +351,14 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
var endValue = (seconds.toFloat() / endTimestamp) * 100 var endValue = (seconds.toFloat() / endTimestamp) * 100
if (endValue > 100F){ if (endValue > 100F){
endTimestamp = timeSeconds endTimestamp = timeSeconds
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US)) toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
endValue = 100F endValue = 100F
} }
if (seconds == 0) { if (seconds == 0) {
toTextInput.editText!!.setTextAndRecalculateWidth(infoUtil.formatIntegerDuration(endTimestamp, Locale.US)) toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}else if (endValue <= rangeSlider.valueFrom.toInt()){ }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()) 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 startTimestamp = (values[0].toInt() * timeSeconds) / 100
val endTimestamp = (values[1].toInt() * timeSeconds) / 100 val endTimestamp = (values[1].toInt() * timeSeconds) / 100
val startTimestampString = infoUtil.formatIntegerDuration(startTimestamp, Locale.US) val startTimestampString = startTimestamp.toStringDuration(Locale.US)
val endTimestampString = infoUtil.formatIntegerDuration(endTimestamp, Locale.US) val endTimestampString = endTimestamp.toStringDuration(Locale.US)
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString) fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString) toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString)

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -18,6 +19,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -48,7 +50,7 @@ import kotlinx.coroutines.withContext
import java.io.File 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 fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
@ -63,6 +65,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
lateinit var author : TextInputLayout lateinit var author : TextInputLayout
lateinit var preferences: SharedPreferences lateinit var preferences: SharedPreferences
lateinit var shownFields: List<String> lateinit var shownFields: List<String>
@SuppressLint("RestrictedApi")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -95,7 +98,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
try { try {
title = view.findViewById(R.id.title_textinput) 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){ if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title) title.editText!!.setText(downloadItem.title)
title.endIconMode = END_ICON_NONE 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 = 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){ if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author) author.editText!!.setText(downloadItem.author)
author.endIconMode = END_ICON_NONE 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) val containers = requireContext().resources.getStringArray(R.array.audio_containers)
var containerPreference = sharedPreferences.getString("audio_format", "Default") var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue) if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer) val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE 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 = val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview) 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)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f (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 //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 { lifecycleScope.launch {
resultViewModel.uiState.collectLatest { res -> resultViewModel.uiState.collectLatest { res ->
@ -436,7 +416,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
shimmerLoadingSubtitle.visibility = View.VISIBLE shimmerLoadingSubtitle.visibility = View.VISIBLE
shimmerLoading.startShimmer() shimmerLoading.startShimmer()
shimmerLoadingSubtitle.startShimmer() shimmerLoadingSubtitle.startShimmer()
updateItem.visibility = View.GONE (updateItem.parent as LinearLayout).visibility = View.GONE
}else{ }else{
title.visibility = View.VISIBLE title.visibility = View.VISIBLE
subtitle.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) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO){ 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) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)
}else{ }else{

View file

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

View file

@ -9,8 +9,9 @@ import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -34,23 +35,21 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R 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.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format 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.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel 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.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker 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.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@ -82,16 +81,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var bottomAppBar: BottomAppBar private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView private lateinit var filesize : TextView
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var type: DownloadViewModel.Type
private lateinit var items: MutableList<DownloadItem>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java] historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
infoUtil = InfoUtil(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) val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
if (Build.VERSION.SDK_INT >= 33){ arguments?.getSerializable("type").apply {
arguments?.getParcelableArrayList("downloads", DownloadItem::class.java)
}else{
arguments?.getParcelableArrayList<DownloadItem>("downloads")
}.apply {
if (this == null){ if (this == null){
dismiss() dismiss()
return return
}else{ }else{
items = this type = this as DownloadViewModel.Type
} }
} }
if (items.isEmpty()) dismiss()
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
@ -138,42 +130,52 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
recyclerView.adapter = listAdapter recyclerView.adapter = listAdapter
recyclerView.enableFastScroll() recyclerView.enableFastScroll()
view.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
setOnClickListener {
recyclerView.scrollToPosition(0)
}
}
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("multipledownloadcard")){ if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("multipledownloadcard")){
val itemTouchHelper = ItemTouchHelper(simpleCallback) val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView) itemTouchHelper.attachToRecyclerView(recyclerView)
} }
listAdapter.submitList(items.toList())
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_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{ scheduleBtn.setOnClickListener{
UiUtil.showDatePicker(parentFragmentManager) { UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
items.forEach { item ->
item.downloadStartTime = it.timeInMillis
} lifecycleScope.launch {
runBlocking { withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(items) downloadViewModel.downloadProcessingDownloads(cal.timeInMillis)
val first = items.first() }
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime)
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() Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
dismiss()
} }
dismiss()
} }
} }
download!!.setOnClickListener { download!!.setOnClickListener {
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
runBlocking { lifecycleScope.launch {
downloadViewModel.queueDownloads(items) withContext(Dispatchers.IO){
downloadViewModel.downloadProcessingDownloads()
}
dismiss()
} }
dismiss()
} }
download.setOnLongClickListener { download.setOnLongClickListener {
@ -182,9 +184,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch{ lifecycleScope.launch{
items.map { it.status = DownloadRepository.Status.Saved.toString() }
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.insertAll(items) downloadViewModel.moveProcessingToSavedCategory()
} }
dismiss() dismiss()
} }
@ -195,7 +196,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomAppBar = view.findViewById(R.id.bottomAppBar) bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type) val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
when(items.first().type){ when(type){
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
} }
@ -212,72 +213,28 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val formatListener = object : OnFormatClickListener { val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) { override fun onFormatClick(selectedFormats: List<FormatTuple>) {
items.forEachIndexed { index, i -> CoroutineScope(Dispatchers.IO).launch {
i.format = selectedFormats[index].format downloadViewModel.updateProcessingFormat(selectedFormats)
if (i.type == DownloadViewModel.Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
} }
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) { override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch(Dispatchers.IO){ CoroutineScope(Dispatchers.IO).launch {
val formatCollection = mutableListOf<List<Format>>() downloadViewModel.updateProcessingAllFormats(allFormats)
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 })
}
} }
} }
override fun onContinueOnBackground() { override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch { requireActivity().lifecycleScope.launch {
val ids = mutableListOf<Long>() withContext(Dispatchers.IO){
runBlocking { downloadViewModel.continueUpdatingFormatsOnBackground()
items.map { it.status = DownloadRepository.Status.Saved.toString() }
ids.addAll(downloadViewModel.repository.insertAll(items))
} }
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.menu[3].icon?.alpha = 30
} }
bottomAppBar.setOnMenuItemClickListener { m: MenuItem -> bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
@ -311,39 +268,38 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
audio!!.setOnClickListener { audio!!.setOnClickListener {
items = downloadViewModel.switchDownloadType(items, DownloadViewModel.Type.audio).toMutableList() CoroutineScope(Dispatchers.IO).launch {
listAdapter.submitList(items.toList()) downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio)
listAdapter.notifyDataSetChanged() withContext(Dispatchers.Main){
updateFileSize(items.map { it.format.filesize }) preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[3].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255 bottomSheet.cancel()
bottomSheet.cancel() }
}
} }
video!!.setOnClickListener { video!!.setOnClickListener {
items = downloadViewModel.switchDownloadType(items, DownloadViewModel.Type.video).toMutableList() CoroutineScope(Dispatchers.IO).launch{
listAdapter.submitList(items.toList()) downloadViewModel.updateProcessingType(DownloadViewModel.Type.video)
listAdapter.notifyDataSetChanged() withContext(Dispatchers.Main){
updateFileSize(items.map { it.format.filesize }) preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[3].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255 bottomSheet.cancel()
bottomSheet.cancel() }
}
} }
command!!.setOnClickListener { command!!.setOnClickListener {
lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch{
items = withContext(Dispatchers.IO){ downloadViewModel.updateProcessingType(DownloadViewModel.Type.command)
downloadViewModel.switchDownloadType(items, DownloadViewModel.Type.command).toMutableList() 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 -> { R.id.format -> {
if (! items.all { it.type == items[0].type }){ lifecycleScope.launch {
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show() val res = withContext(Dispatchers.IO){
}else{ downloadViewModel.checkIfAllProcessingItemsHaveSameType()
if (items.first().type == DownloadViewModel.Type.command){ }
lifecycleScope.launch { 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) { UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel) {
val format = Format( val format = Format(
it.first().title, it.first().title,
@ -375,15 +335,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
it.joinToString(" ") { c -> c.content } it.joinToString(" ") { c -> c.content }
) )
items.forEach { f -> CoroutineScope(Dispatchers.IO).launch {
f.format = format downloadViewModel.updateProcessingCommandFormat(format)
} }
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
} }
} }else{
}else{ val items = withContext(Dispatchers.IO){
lifecycleScope.launch { downloadViewModel.getProcessingDownloads()
}
val flatFormatCollection = items.map { it.allFormats }.flatten() val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){ 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 } 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 -> { R.id.more -> {
if (! items.all { it.type == items[0].type }) { lifecycleScope.launch {
Toast.makeText( val res = withContext(Dispatchers.IO){
requireContext(), downloadViewModel.checkIfAllProcessingItemsHaveSameType()
getString(R.string.format_filtering_hint), }
Toast.LENGTH_SHORT if (!res.first) {
).show() Toast.makeText(
}else{ requireContext(),
val scale = resources.displayMetrics.density getString(R.string.format_filtering_hint),
val padding = (40*scale*0.5f).toInt() Toast.LENGTH_SHORT
).show()
}else{
val scale = resources.displayMetrics.density
val padding = (40*scale*0.5f).toInt()
when(items[0].type){ when(res.second){
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
val bottomSheet = BottomSheetDialog(requireContext()) val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.adjust_audio) bottomSheet.setContentView(R.layout.adjust_audio)
val sheetView = bottomSheet.findViewById<View>(android.R.id.content)!! val sheetView = bottomSheet.findViewById<View>(android.R.id.content)!!
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding) sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
UiUtil.configureAudio( val items = withContext(Dispatchers.IO){
sheetView, downloadViewModel.getProcessingDownloads()
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")
} }
)
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( UiUtil.configureAudio(
sheetView, sheetView,
requireActivity(), requireActivity(),
items, items,
embedSubsClicked = {checked -> embedThumbClicked = {enabled ->
items.forEach { it.videoPreferences.embedSubs = checked } items.forEach {
}, it.audioPreferences.embedThumb = enabled
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]) }
} }
} CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
bottomSheet.dismiss() },
}, cropThumbClicked = {enabled ->
cutClicked = {}, items.forEach {
updateDataClicked = {}, it.audioPreferences.cropThumb = enabled
filenameTemplateSet = { checked -> }
items.forEach { it.customFileNameTemplate = checked } CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
}, },
saveSubtitlesClicked = {checked -> splitByChaptersClicked = {enabled ->
items.forEach { it.videoPreferences.writeSubs = checked } items.forEach {
}, it.audioPreferences.splitByChapters = enabled
subtitleLanguagesSet = {value -> }
items.forEach { it.videoPreferences.subsLanguages = value } CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
}, },
removeAudioClicked = {checked -> filenameTemplateSet = {template ->
items.forEach { it.videoPreferences.removeAudio = checked } items.forEach {
}, it.customFileNameTemplate = template
extraCommandsClicked = { }
val callback = object : ExtraCommandsListener { requireActivity().lifecycleScope.launch {
override fun onChangeExtraCommand(c: String) { items.forEach { downloadViewModel.updateDownload(it) }
items.forEach { it.extraCommands = c } }
bottomSheet.dismiss() 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) val bottomSheetDialog = AddExtraCommandsDialog(null, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands") 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() UiUtil.configureVideo(
val displayMetrics = DisplayMetrics() sheetView,
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) requireActivity(),
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels items,
bottomSheet.window!!.setLayout( embedSubsClicked = {checked ->
ViewGroup.LayoutParams.MATCH_PARENT, items.forEach { it.videoPreferences.embedSubs = checked }
ViewGroup.LayoutParams.MATCH_PARENT CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
) },
} addChaptersClicked = {checked ->
DownloadViewModel.Type.command -> { 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 true
} }
CoroutineScope(Dispatchers.IO).launch{ lifecycleScope.launch {
downloadViewModel.uiState.collectLatest { res -> downloadViewModel.processingDownloads.collectLatest {
if (res.errorMessage != null) { listAdapter.submitList(it)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
kotlin.runCatching { updateFileSize(it.map { it2 -> it2.format.filesize })
UiUtil.handleDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
res,
downloadViewModel,
historyViewModel)
}
}
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
errorMessage = null,
actions = null
)
} }
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>){ private fun updateFileSize(items: List<Long>){
if (items.all { it > 0L }){ if (items.all { it > 0L }){
filesize.visibility = View.VISIBLE 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{ }else{
filesize.visibility = View.GONE filesize.visibility = View.GONE
} }
@ -603,18 +607,21 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
Intent.FLAG_GRANT_WRITE_URI_PERMISSION 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()) 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 { lifecycleScope.launch {
val item = items.find { it.url == itemURL } ?: return@launch var item = withContext(Dispatchers.IO){
val position = items.indexOf(item) downloadViewModel.getItemByID(id)
}
val bottomSheet = BottomSheetDialog(requireContext()) val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -635,26 +642,31 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
audio!!.setOnClickListener { audio!!.setOnClickListener {
items[position] = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.audio).first() lifecycleScope.launch {
listAdapter.submitList(items.toList()) item = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.audio).first()
listAdapter.notifyItemChanged(position) withContext(Dispatchers.IO){
bottomSheet.cancel() downloadViewModel.updateDownload(item)
}
bottomSheet.cancel()
}
} }
video!!.setOnClickListener { video!!.setOnClickListener {
items[position] = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.video).first() lifecycleScope.launch {
listAdapter.submitList(items.toList()) item = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.video).first()
listAdapter.notifyItemChanged(position) withContext(Dispatchers.IO){
bottomSheet.cancel() downloadViewModel.updateDownload(item)
}
bottomSheet.cancel()
}
} }
command!!.setOnClickListener { command!!.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
items[position] = withContext(Dispatchers.IO){ item = downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.command).first()
downloadViewModel.switchDownloadType(listOf(item), DownloadViewModel.Type.command).first() withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
} }
listAdapter.submitList(items.toList())
listAdapter.notifyItemChanged(position)
bottomSheet.cancel() bottomSheet.cancel()
} }
} }
@ -670,60 +682,71 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
override fun onCardClick(itemURL: String) { override fun onCardClick(id: Long) {
lifecycleScope.launch{ lifecycleScope.launch{
val downloadItem = items.find { it.url == itemURL }
val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
val resultItem = withContext(Dispatchers.IO){ val resultItem = withContext(Dispatchers.IO){
resultViewModel.getItemByURL(downloadItem!!.url)!! resultViewModel.getItemByURL(downloadItem.url)!!
} }
if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){ if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem!!, this@DownloadMultipleBottomSheetDialog) val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, this@DownloadMultipleBottomSheetDialog)
bottomSheet.show(parentFragmentManager, "configureDownloadSingleSheet") bottomSheet.show(parentFragmentManager, "configureDownloadSingleSheet")
} }
} }
} }
override fun onDelete(itemURL: String) { override fun onDelete(id: Long) {
val deletedItem = items.find { it.url == itemURL } ?: return lifecycleScope.launch {
val position = items.indexOf(deletedItem) val deletedItem = withContext(Dispatchers.IO){
UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){ downloadViewModel.getItemByID(id)
items.remove(deletedItem) } ?: return@launch
listAdapter.submitList(items.toList())
if (items.isNotEmpty()){ UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) lifecycleScope.launch {
.setAction(getString(R.string.undo)) { val count = withContext(Dispatchers.IO){
items.add(position, deletedItem) downloadViewModel.getProcessingDownloadsCount()
listAdapter.submitList(items.toList()) }
}.show() downloadViewModel.deleteDownload(id)
}else{
dismiss() 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 onClick(p0: View?) {
} }
override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) { override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) {
val i = items.indexOf(items.find { it.url == item.url }) lifecycleScope.launch {
items[i] = item withContext(Dispatchers.IO){
if (! items.all { it.type == items[0].type }){ downloadViewModel.updateDownload(item)
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
} }
listAdapter.notifyDataSetChanged()
} }
}
if (items.count { it.type == DownloadViewModel.Type.command } == 0){ override fun onDismiss(dialog: DialogInterface) {
bottomAppBar.menu[2].icon?.alpha = 255 requireActivity().lifecycleScope.launch {
}else{ downloadViewModel.deleteProcessing()
bottomAppBar.menu[2].icon?.alpha = 30
} }
super.onDismiss(dialog)
listAdapter.submitList(items.toList())
} }
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
@ -734,22 +757,29 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = items[position] lifecycleScope.launch {
items.remove(deletedItem) val deletedItem = withContext(Dispatchers.IO){
listAdapter.submitList(items.toList()) downloadViewModel.getItemByID(itemID)
if (items.isNotEmpty()){ }
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) val count = withContext(Dispatchers.IO){
.setAction(getString(R.string.undo)) { downloadViewModel.getProcessingDownloadsCount()
items.add(position, deletedItem) }
listAdapter.submitList(items.toList()) withContext(Dispatchers.IO){
}.show() downloadViewModel.deleteDownload(deletedItem.id)
}else{ }
dismiss()
}
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.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -34,23 +35,19 @@ import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView 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
import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM 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.END_ICON_NONE
import com.google.android.material.textfield.TextInputLayout.EndIconMode
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File 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 fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
@ -67,6 +64,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
lateinit var downloadItem: DownloadItem lateinit var downloadItem: DownloadItem
@SuppressLint("RestrictedApi")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -100,7 +98,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
try { try {
title = view.findViewById(R.id.title_textinput) 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){ if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title) title.editText!!.setText(downloadItem.title)
title.endIconMode = END_ICON_NONE 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 = 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){ if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author) author.editText!!.setText(downloadItem.author)
author.endIconMode = END_ICON_NONE 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 containers = requireContext().resources.getStringArray(R.array.video_containers)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer) val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE 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 = val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview) view.findViewById<AutoCompleteTextView>(R.id.container_textview)
var containerPreference = sharedPreferences.getString("video_format", "Default") var containerPreference = sharedPreferences.getString("video_format", "Default")
@ -329,6 +336,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
saveSubtitlesClicked = { saveSubtitlesClicked = {
downloadItem.videoPreferences.writeSubs = it downloadItem.videoPreferences.writeSubs = it
}, },
saveAutoSubtitlesClicked = {
downloadItem.videoPreferences.writeAutoSubs = it
},
subtitleLanguagesSet = { subtitleLanguagesSet = {
downloadItem.videoPreferences.subsLanguages = it 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) 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 { 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.ui.adapter.PlaylistAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity import com.deniscerri.ytdlnis.receiver.ShareActivity
@ -31,6 +32,7 @@ import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -197,11 +199,13 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
downloadItems.add(i) downloadItems.add(i)
} }
withContext(Dispatchers.Main){ CoroutineScope(Dispatchers.IO).launch {
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog, bundleOf( downloadViewModel.insertToProcessing(downloadItems)
Pair("downloads", downloadItems)
))
} }
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("type", downloadItems[0].type)
))
} }
dismiss() dismiss()

View file

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.get
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -17,8 +18,10 @@ import androidx.viewpager2.widget.ViewPager2
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
@ -136,6 +139,24 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.deleteSaved() 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){ }catch (e: Exception){
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() 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.chip.ChipGroup
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -80,7 +81,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private var fragmentContext: Context? = null private var fragmentContext: Context? = null
private var layoutinflater: LayoutInflater? = null private var layoutinflater: LayoutInflater? = null
private var topAppBar: MaterialToolbar? = null private var topAppBar: MaterialToolbar? = null
private var recyclerView: RecyclerView? = null private lateinit var recyclerView: RecyclerView
private var historyAdapter: HistoryAdapter? = null private var historyAdapter: HistoryAdapter? = null
private var sortSheet: BottomSheetDialog? = null private var sortSheet: BottomSheetDialog? = null
private var uiHandler: Handler? = null private var uiHandler: Handler? = null
@ -128,8 +129,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
requireActivity() requireActivity()
) )
recyclerView = view.findViewById(R.id.recyclerviewhistorys) recyclerView = view.findViewById(R.id.recyclerviewhistorys)
recyclerView?.adapter = historyAdapter recyclerView.adapter = historyAdapter
recyclerView?.enableFastScroll() recyclerView.enableFastScroll()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("history")){ 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) 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 noResults?.visibility = GONE
@ -185,11 +186,11 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
lifecycleScope.launch{ lifecycleScope.launch{
downloadViewModel.uiState.collectLatest { res -> downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.errorMessage != null) { if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
kotlin.runCatching { kotlin.runCatching {
UiUtil.handleDownloadsResponse( UiUtil.handleExistingDownloadsResponse(
requireActivity(), requireActivity(),
requireActivity().lifecycleScope, requireActivity().lifecycleScope,
requireActivity().supportFragmentManager, requireActivity().supportFragmentManager,
@ -198,9 +199,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel) historyViewModel)
} }
} }
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState( downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
errorMessage = null, mutableListOf(),
actions = null mutableListOf()
) )
} }
} }
@ -396,6 +397,17 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
command.isChecked = false 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>) { private fun updateWebsiteChips(list : List<HistoryItem>) {
@ -577,7 +589,18 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyAdapter!!.notifyItemChanged(position) historyAdapter!!.notifyItemChanged(position)
UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(), UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(),
delete = { item, deleteFile -> 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 -> { 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.repository.CommandTemplateRepository.CommandTemplateSortType
import com.deniscerri.ytdlnis.database.DBManager.SORTING import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar

View file

@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -39,6 +39,7 @@ import com.google.android.material.textfield.TextInputLayout
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -162,38 +163,51 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
} }
private fun showDialog(url: String){ private fun showDialog(url: String){
val builder = MaterialAlertDialogBuilder(requireContext()) lifecycleScope.launch {
builder.setTitle(getString(R.string.cookies)) var item = withContext(Dispatchers.IO){
val inputLayout = layoutInflater.inflate(R.layout.textinput, null) cookiesViewModel.getByURL(url)
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)
}
// handle the negative button of the alert dialog
builder.setNegativeButton( val builder = MaterialAlertDialogBuilder(requireContext())
getString(R.string.cancel) builder.setTitle(getString(R.string.cookies))
) { dialog: DialogInterface?, which: Int -> } val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val dialog = builder.create() val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
editText.doOnTextChanged { text, start, before, count -> 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.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 { companion object {

View file

@ -35,6 +35,7 @@ class MoreFragment : Fragment() {
private lateinit var commandTemplates: TextView private lateinit var commandTemplates: TextView
private lateinit var downloadQueue: TextView private lateinit var downloadQueue: TextView
private lateinit var cookies: TextView private lateinit var cookies: TextView
private lateinit var observeSources: TextView
private lateinit var terminateApp: TextView private lateinit var terminateApp: TextView
private lateinit var settings: TextView private lateinit var settings: TextView
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
@ -58,6 +59,7 @@ class MoreFragment : Fragment() {
commandTemplates = view.findViewById(R.id.command_templates) commandTemplates = view.findViewById(R.id.command_templates)
downloadQueue = view.findViewById(R.id.download_queue) downloadQueue = view.findViewById(R.id.download_queue)
cookies = view.findViewById(R.id.cookies) cookies = view.findViewById(R.id.cookies)
observeSources = view.findViewById(R.id.observe_sources)
terminateApp = view.findViewById(R.id.terminate) terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings) settings = view.findViewById(R.id.settings)
@ -82,6 +84,10 @@ class MoreFragment : Fragment() {
findNavController().navigate(R.id.cookiesFragment) findNavController().navigate(R.id.cookiesFragment)
} }
observeSources.setOnClickListener {
findNavController().navigate(R.id.observeSourcesFragment)
}
terminateApp.setOnClickListener { terminateApp.setOnClickListener {
if (mainSharedPreferences.getBoolean("ask_terminate_app", true)){ if (mainSharedPreferences.getBoolean("ask_terminate_app", true)){
var doNotShowAgain = false 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.app.ActionBar
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE import android.content.Context.CLIPBOARD_SERVICE
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -13,10 +14,12 @@ import android.widget.HorizontalScrollView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.edit
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
@ -39,6 +42,7 @@ class DownloadLogFragment : Fragment() {
private lateinit var copyLog : ExtendedFloatingActionButton private lateinit var copyLog : ExtendedFloatingActionButton
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel private lateinit var logViewModel: LogViewModel
private lateinit var sharedPreferences: SharedPreferences
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -47,6 +51,7 @@ class DownloadLogFragment : Fragment() {
): View? { ): View? {
mainActivity = activity as MainActivity mainActivity = activity as MainActivity
mainActivity.hideBottomNavigation() mainActivity.hideBottomNavigation()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
return inflater.inflate(R.layout.fragment_download_log, container, false) return inflater.inflate(R.layout.fragment_download_log, container, false)
} }
@ -141,9 +146,13 @@ class DownloadLogFragment : Fragment() {
slider?.apply { slider?.apply {
this.valueFrom = 0f this.valueFrom = 0f
this.valueTo = 10f this.valueTo = 10f
this.value = 2f this.value = sharedPreferences.getFloat("log_zoom", 2f)
content.setCustomTextSize(this.value + 13f)
this.addOnChangeListener { slider, value, fromUser -> this.addOnChangeListener { slider, value, fromUser ->
content.setCustomTextSize(value + 13f) 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.PreferenceManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R 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.util.UiUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler import com.deniscerri.ytdlnis.work.AlarmScheduler
import java.util.Calendar import java.util.Calendar

View file

@ -21,6 +21,7 @@ import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -278,10 +279,15 @@ class TerminalFragment : Fragment() {
slider?.apply { slider?.apply {
this.valueFrom = 0f this.valueFrom = 0f
this.valueTo = 10f 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 -> this.addOnChangeListener { slider, value, fromUser ->
output?.setCustomTextSize(value + 13f) output?.setCustomTextSize(value + 13f)
input?.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 package com.deniscerri.ytdlnis.util
import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
@ -11,25 +12,26 @@ import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.TypedValue import android.util.TypedValue
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
import android.view.animation.Interpolator
import android.widget.EditText import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import androidx.lifecycle.withStarted import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.neo.highlight.core.Highlight import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme import com.neo.highlight.util.scheme.ColorScheme
@ -37,7 +39,9 @@ import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.abs
object Extensions { object Extensions {
@ -110,6 +114,7 @@ object Extensions {
drawable.paint.color = context.getColor(android.R.color.transparent) drawable.paint.color = context.getColor(android.R.color.transparent)
FastScrollerBuilder(this) FastScrollerBuilder(this)
.useMd2Style()
.setTrackDrawable(drawable) .setTrackDrawable(drawable)
.build() .build()
} }
@ -187,4 +192,41 @@ object Extensions {
this.setTextSize(TypedValue.COMPLEX_UNIT_SP, newSize) 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" 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 { fun getDefaultTerminalPath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/TERMINAL_CACHE" 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.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.getIntByAny import com.deniscerri.ytdlnis.util.Extensions.getIntByAny
import com.deniscerri.ytdlnis.util.Extensions.getStringByAny import com.deniscerri.ytdlnis.util.Extensions.getStringByAny
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL 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")) val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))
itm?.playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName } itm?.playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id" itm?.playlistURL = "https://www.youtube.com/playlist?list=$id"
itm?.playlistIndex = (i+1)
items.add(itm) items.add(itm)
} }
} }
@ -187,7 +187,7 @@ class InfoUtil(private val context: Context) {
Html.fromHtml(obj.getString("uploaderName").toString()).toString() 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 thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList() val formats : ArrayList<Format> = ArrayList()
@ -206,6 +206,9 @@ class InfoUtil(private val context: Context) {
}else{ }else{
formatObj.format_note = formatObj.format_note + " Audio" formatObj.format_note = formatObj.format_note + " Audio"
} }
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) { }catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -223,6 +226,9 @@ class InfoUtil(private val context: Context) {
val formatObj = Gson().fromJson(format.toString(), Format::class.java) val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{ try{
formatObj.vcodec = format.getString("codec") formatObj.vcodec = format.getString("codec")
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) { }catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -453,6 +459,10 @@ class InfoUtil(private val context: Context) {
request.addOption("--compat-options", "manifest-filesize-approx") request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) request.addOption("--cookies", it)
@ -505,6 +515,10 @@ class InfoUtil(private val context: Context) {
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
@ -588,6 +602,10 @@ class InfoUtil(private val context: Context) {
request.addOption("--compat-options", "manifest-filesize-approx") request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) request.addOption("--cookies", it)
@ -625,7 +643,7 @@ class InfoUtil(private val context: Context) {
val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id") val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString() var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){ if (duration != "-1"){
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
} }
var thumb: String? = "" var thumb: String? = ""
@ -724,6 +742,7 @@ class InfoUtil(private val context: Context) {
}catch (ignored: Exception){} }catch (ignored: Exception){}
} }
val formatProper = Gson().fromJson(format.toString(), Format::class.java) val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.format_note == null) continue if (formatProper.format_note == null) continue
if (format.has("format_note")){ if (format.has("format_note")){
if (!formatProper!!.format_note.contains("audio only", true)) { if (!formatProper!!.format_note.contains("audio only", true)) {
@ -736,10 +755,20 @@ class InfoUtil(private val context: Context) {
} }
if (formatProper.format_note == "storyboard") continue if (formatProper.format_note == "storyboard") continue
formatProper.container = format.getString("ext") 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) formats.add(formatProper)
} }
} }
return formats return formats
} }
@ -751,6 +780,10 @@ class InfoUtil(private val context: Context) {
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) request.addOption("--cookies", it)
@ -780,7 +813,7 @@ class InfoUtil(private val context: Context) {
var duration = jsonObject.getIntByAny("duration").toString() var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){ if (duration != "-1"){
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
} }
var thumb: String? = "" var thumb: String? = ""
@ -871,19 +904,7 @@ class InfoUtil(private val context: Context) {
return duration 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?> { fun getStreamingUrlAndChapters(url: String) : MutableList<String?> {
try { try {
@ -904,6 +925,10 @@ class InfoUtil(private val context: Context) {
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--socket-timeout", "5") request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) request.addOption("--cookies", it)
@ -955,11 +980,16 @@ class InfoUtil(private val context: Context) {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ 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) YoutubeDLRequest(downloadItem.url)
}else{ }else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply { 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 (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
if (sharedPreferences.getBoolean("restrict_filenames", true)) request.addOption("--restrict-filenames") 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)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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 preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec) val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec)
val aCodecPref = runCatching { context.getStringArray(R.array.audio_codec_values_ytdlp)[aCodecPrefIndex] }.getOrElse { "" } 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("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", subFormat ?: "srt") request.addOption("--convert-subtitles", subFormat ?: "srt")
} }
} }
if (downloadItem.videoPreferences.embedSubs) { if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs") request.addOption("--embed-subs")
} }
if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs){ if(downloadItem.videoPreferences.writeAutoSubs){
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
request.addOption("--write-auto-subs") 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){ if (downloadItem.videoPreferences.removeAudio){
request.addOption("--ppa", "ffmpeg:-an") request.addOption("--ppa", "ffmpeg:-an")
} }

View file

@ -126,6 +126,27 @@ class NotificationUtil(var context: Context) {
.build() .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( fun createDownloadServiceNotification(
pendingIntent: PendingIntent?, pendingIntent: PendingIntent?,
title: String?, title: String?,

View file

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

View file

@ -5,8 +5,8 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver import com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver import com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers.AlarmStopReceiver
import java.util.Calendar import java.util.Calendar
class AlarmScheduler(private val context: Context) { 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.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration 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.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
@ -228,7 +229,7 @@ class DownloadWorker(
val file = File(this) val file = File(this)
var duration = downloadItem.duration var duration = downloadItem.duration
val d = file.getMediaDuration(context) 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.filesize = file.length()
downloadItem.format.container = file.extension 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end"> android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab" android:id="@+id/copied_url_fab"
android:visibility="gone" android:visibility="gone"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" 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:contentDescription="@string/link_you_copied"
android:src="@drawable/ic_clipboard" app:icon="@drawable/ic_clipboard"
android:text="@string/download_all"
app:useCompatPadding="true" /> app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -253,6 +254,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all" android:text="@string/download_all"
app:icon="@drawable/ic_down" /> app:icon="@drawable/ic_down" />

View file

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

View file

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

View file

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

View file

@ -174,6 +174,14 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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 <com.google.android.material.chip.Chip
android:id="@+id/subtitle_languages" android:id="@+id/subtitle_languages"
style="@style/Widget.Material3.Chip.Assist.Elevated" style="@style/Widget.Material3.Chip.Assist.Elevated"

View file

@ -1,9 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/existing_list" <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> 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"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" 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_width="match_parent"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:checkable="true" android:checkable="true"
@ -23,22 +23,42 @@
android:gravity="center_vertical" android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="10dp" 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_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<Button <LinearLayout
android:id="@+id/already_exists_edit" android:id="@+id/already_exists_buttons"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent"
app:icon="@drawable/ic_edit"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintEnd_toEndOf="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> </androidx.constraintlayout.widget.ConstraintLayout>

View file

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

View file

@ -45,6 +45,29 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent"> 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 <TextView
android:id="@+id/filesize" android:id="@+id/filesize"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"

View file

@ -298,6 +298,41 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </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> </LinearLayout>

View file

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

View file

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

View file

@ -57,6 +57,16 @@
<View style="@style/Divider.Vertical"/> <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 <com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content" android:layout_width="wrap_content"
app:selectionRequired="false" app:selectionRequired="false"

View file

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

View file

@ -100,6 +100,21 @@
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/baseline_cookie_24" /> 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 <TextView
android:id="@+id/terminate" android:id="@+id/terminate"
android:layout_width="match_parent" 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:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:drawablePadding="20dp" android:drawablePadding="20dp"
android:maxLength="100"
android:ellipsize="end" android:ellipsize="end"
android:focusable="true" android:focusable="true"
android:gravity="center_vertical" android:gravity="center_vertical"

View file

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

View file

@ -29,6 +29,10 @@
android:icon="@drawable/baseline_cookie_24" android:icon="@drawable/baseline_cookie_24"
android:checkable="true" android:checkable="true"
android:id="@+id/cookiesFragment"/> 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" <item android:title="@string/kill_app"
android:checkable="true" android:checkable="true"
android:icon="@drawable/ic_power" 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 <action
android:id="@+id/action_moreFragment_to_terminalActivity" android:id="@+id/action_moreFragment_to_terminalActivity"
app:destination="@id/terminalActivity" /> app:destination="@id/terminalActivity" />
<action
android:id="@+id/action_moreFragment_to_observeSourcesFragment"
app:destination="@id/observeSourcesFragment" />
</fragment> </fragment>
<activity <activity
android:id="@+id/terminalActivity" android:id="@+id/terminalActivity"
@ -163,9 +166,29 @@
<dialog <dialog
android:id="@+id/downloadMultipleBottomSheetDialog2" android:id="@+id/downloadMultipleBottomSheetDialog2"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog" 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 <dialog
android:id="@+id/cutVideoBottomSheetDialog" android:id="@+id/cutVideoBottomSheetDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.CutVideoBottomSheetDialog" android:name="com.deniscerri.ytdlnis.ui.downloadcard.CutVideoBottomSheetDialog"
android:label="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> </navigation>

View file

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

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