This commit is contained in:
zaednasr 2024-02-25 19:23:38 +01:00
parent 39c086aa6b
commit 177ba4881c
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
147 changed files with 2645 additions and 1341 deletions

View file

@ -10,7 +10,7 @@ plugins {
def properties = new Properties()
def versionMajor = 1
def versionMinor = 7
def versionPatch = 2
def versionPatch = 3
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = ""

View file

@ -0,0 +1,724 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "3f4252e3a5d5fbf3d4706a4d999055f1",
"entities": [
{
"tableName": "results",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `creationTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistTitle",
"columnName": "playlistTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formats",
"columnName": "formats",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "chapters",
"columnName": "chapters",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "playlistURL",
"columnName": "playlistURL",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "playlistIndex",
"columnName": "playlistIndex",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "creationTime",
"columnName": "creationTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0, `command` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadPath",
"columnName": "downloadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadId",
"columnName": "downloadId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "downloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `extraCommands` TEXT NOT NULL DEFAULT '', `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "container",
"columnName": "container",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Default'"
},
{
"fieldPath": "downloadSections",
"columnName": "downloadSections",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "allFormats",
"columnName": "allFormats",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadPath",
"columnName": "downloadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadSize",
"columnName": "downloadSize",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistTitle",
"columnName": "playlistTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "audioPreferences",
"columnName": "audioPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoPreferences",
"columnName": "videoPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extraCommands",
"columnName": "extraCommands",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "customFileNameTemplate",
"columnName": "customFileNameTemplate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "SaveThumb",
"columnName": "SaveThumb",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Queued'"
},
{
"fieldPath": "downloadStartTime",
"columnName": "downloadStartTime",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "logID",
"columnName": "logID",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "playlistURL",
"columnName": "playlistURL",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "playlistIndex",
"columnName": "playlistIndex",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "commandTemplates",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `useAsExtraCommand` INTEGER NOT NULL DEFAULT 0, `useAsExtraCommandAudio` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandVideo` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "useAsExtraCommand",
"columnName": "useAsExtraCommand",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "useAsExtraCommandAudio",
"columnName": "useAsExtraCommandAudio",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "useAsExtraCommandVideo",
"columnName": "useAsExtraCommandVideo",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistory",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "templateShortcuts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cookies",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "logs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadType",
"columnName": "downloadType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadTime",
"columnName": "downloadTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "terminalDownloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL, `log` TEXT DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "log",
"columnName": "log",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "observeSources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `status` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyWeekDay` TEXT NOT NULL, `everyMonthDay` INTEGER NOT NULL, `everyTime` INTEGER NOT NULL, `startsTime` INTEGER NOT NULL, `startsMonth` TEXT NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `getOnlyNewUploads` 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": "getOnlyNewUploads",
"columnName": "getOnlyNewUploads",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "retryMissingDownloads",
"columnName": "retryMissingDownloads",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alreadyProcessedLinks",
"columnName": "alreadyProcessedLinks",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3f4252e3a5d5fbf3d4706a4d999055f1')"
]
}
}

View file

@ -11,7 +11,6 @@
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

View file

@ -13,16 +13,13 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowInsets
import android.widget.CheckBox
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach

View file

@ -1,14 +1,30 @@
package com.deniscerri.ytdlnis.database
import android.content.Context
import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.work.impl.Migration_11_12
import androidx.work.impl.Migration_12_13
import com.deniscerri.ytdlnis.database.dao.*
import com.deniscerri.ytdlnis.database.models.*
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.dao.CookieDao
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.HistoryDao
import com.deniscerri.ytdlnis.database.dao.LogDao
import com.deniscerri.ytdlnis.database.dao.ObserveSourcesDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.dao.SearchHistoryDao
import com.deniscerri.ytdlnis.database.dao.TerminalDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.models.TerminalItem
@TypeConverters(Converters::class)
@Database(
@ -24,7 +40,7 @@ import com.deniscerri.ytdlnis.database.models.*
TerminalItem::class,
ObserveSourcesItem::class
],
version = 14,
version = 15,
autoMigrations = [
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3),
@ -39,6 +55,7 @@ import com.deniscerri.ytdlnis.database.models.*
AutoMigration (from = 11, to = 12),
AutoMigration (from = 12, to = 13),
// AutoMigration (from = 13, to = 14) MANUALLY HANDLED
AutoMigration (from = 14, to = 15),
]
)
abstract class DBManager : RoomDatabase(){

View file

@ -1,6 +1,10 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow

View file

@ -1,6 +1,10 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.deniscerri.ytdlnis.database.models.CookieItem
import kotlinx.coroutines.flow.Flow
@ -10,7 +14,7 @@ interface CookieDao {
fun getAllCookies() : List<CookieItem>
@Query("SELECT * FROM cookies WHERE url = :url LIMIT 1")
fun getByURL(url: String) : CookieItem
fun getByURL(url: String) : CookieItem?
@Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookiesFlow() : Flow<List<CookieItem>>

View file

@ -1,7 +1,14 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.database.models.Format
@ -19,14 +26,14 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloads() : Flow<List<DownloadItem>>
@Query("SELECT COUNT(*) FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')")
fun getActiveDownloadsCountFlow() : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:statuses)")
fun getDownloadsCountFlow(statuses: List<String>) : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatusFlow(status : List<String>) : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatus(status : List<String>) : Int
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:statuses)")
fun getDownloadsCountByStatus(statuses : List<String>) : Int
@Query("""
@ -79,6 +86,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT id FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloadsListIDs() : List<Long>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloads() : PagingSource<Int, DownloadItemSimple>
@ -207,28 +217,30 @@ interface DownloadDao {
fun getURLsByStatus(status: List<String>) : List<String>
@Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)")
fun resetScheduleTimeForItems(list: List<Long>)
suspend fun resetScheduleTimeForItems(list: List<Long>)
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
fun reQueueDownloadItems(list: List<Long>)
suspend fun reQueueDownloadItems(list: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing'")
fun updateProcessingtoSavedStatus()
@Transaction
fun putAtTopOfTheQueue(id: Long){
val downloads = getQueuedDownloadsList()
val newID = downloads.first().id
suspend fun putAtTopOfTheQueue(existingIDs: List<Long>){
val downloads = getQueuedDownloadsListIDs()
val newIDs = downloads.sortedBy { it }.take(existingIDs.size)
updateDownloadID(id, -id)
resetScheduleTimeForItems(listOf(-id))
downloads.reversed().dropWhile { it.id == id }.forEach {
updateDownloadID(it.id, it.id + 1)
resetScheduleTimeForItems(existingIDs)
existingIDs.forEach { updateDownloadID(it, -it) }
downloads.filter { !existingIDs.contains(it) }.reversed().forEach {
updateDownloadID(it, it + existingIDs.size)
}
existingIDs.forEachIndexed { idx, it ->
updateDownloadID(-it, newIDs[idx])
}
updateDownloadID(-id, newID)
}
@Query("Update downloads set id=:newId where id=:id")
fun updateDownloadID(id: Long, newId: Long)
suspend fun updateDownloadID(id: Long, newId: Long)
}

View file

@ -1,6 +1,10 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.deniscerri.ytdlnis.database.models.HistoryItem
import kotlinx.coroutines.flow.Flow

View file

@ -1,6 +1,11 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.deniscerri.ytdlnis.database.models.LogItem
import kotlinx.coroutines.flow.Flow

View file

@ -1,7 +1,10 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CookieItem
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import kotlinx.coroutines.flow.Flow

View file

@ -1,6 +1,11 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.deniscerri.ytdlnis.database.models.ResultItem
import kotlinx.coroutines.flow.Flow
@ -27,6 +32,11 @@ interface ResultDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMultiple(items: List<ResultItem?>) : List<Long>
@Transaction
suspend fun insertMultipleNoDuplicates(items: List<ResultItem>) : List<Long> {
return insertMultiple(items.filter { getResultByURL(it.url) == null })
}
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ResultItem)

View file

@ -1,6 +1,9 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
@Dao

View file

@ -1,7 +1,11 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow
@Dao
@ -28,16 +32,7 @@ interface TerminalDao {
fun updateLog(line: String, id: Long){
val t = getTerminalById(id) ?: return
val log = t.log ?: ""
val lines = log.split("\n")
//clean dublicate progress + add newline
var newLine = line
if (newLine.contains("[download")){
newLine = "[download]" + line.split("[download]").last()
}
val l = lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}"
updateTerminalLog(l, id)
updateTerminalLog(log.appendLineToLog(line), id)
}
@Insert

View file

@ -7,8 +7,6 @@ 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
@ -32,6 +30,8 @@ data class ObserveSourcesItem(
var endsAfterCount: Int,
@ColumnInfo(defaultValue = "0")
var runCount: Int,
@ColumnInfo(defaultValue = "0")
var getOnlyNewUploads: Boolean,
var retryMissingDownloads: Boolean,
var alreadyProcessedLinks : MutableList<String>
) : Parcelable

View file

@ -1,10 +1,5 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
data class SearchSuggestionItem(
var text: String,
val type: SearchSuggestionType,

View file

@ -13,5 +13,6 @@ data class VideoPreferences (
var writeAutoSubs: Boolean = false,
var subsLanguages: String = "en.*,.*-orig",
var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false
var removeAudio: Boolean = false,
var alsoDownloadAsAudio: Boolean = false
) : Parcelable

View file

@ -11,7 +11,7 @@ class CookieRepository(private val cookieDao: CookieDao) {
return cookieDao.getAllCookies()
}
fun getByURL(url: String) : CookieItem {
fun getByURL(url: String) : CookieItem? {
return cookieDao.getByURL(url)
}

View file

@ -1,15 +1,32 @@
package com.deniscerri.ytdlnis.database.repository
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.os.Looper
import android.widget.Toast
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
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.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.work.DownloadWorker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File
import java.util.concurrent.TimeUnit
class DownloadRepository(private val downloadDao: DownloadDao) {
@ -36,12 +53,20 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getSavedDownloads()}
)
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCountFlow()
val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.PausedReQueued).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued, Status.QueuedPaused).toListString())
val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString())
val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString())
val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString())
enum class Status {
Active, ActivePaused, PausedReQueued, Queued, QueuedPaused, Error, Cancelled, Saved, Processing
}
private fun List<Status>.toListString() : List<String>{
return this.map { it.toString() }
}
suspend fun insert(item: DownloadItem) : Long {
return downloadDao.insert(item)
}
@ -162,4 +187,53 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.removeAllLogID()
}
@SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) {
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)
.setInputData(inputData.build())
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()
}
}
}
}

View file

@ -1,11 +1,11 @@
package com.deniscerri.ytdlnis.database.repository
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.dao.HistoryDao
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import com.deniscerri.ytdlnis.database.DBManager.SORTING
class HistoryRepository(private val historyDao: HistoryDao) {
val items : Flow<List<HistoryItem>> = historyDao.getAllHistory()

View file

@ -2,9 +2,8 @@ package com.deniscerri.ytdlnis.database.repository
import com.deniscerri.ytdlnis.database.dao.LogDao
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow
import java.util.regex.MatchResult
import java.util.regex.Pattern
class LogRepository(private val logDao: LogDao) {
val items : Flow<List<LogItem>> = logDao.getAllLogsFlow()
@ -43,16 +42,7 @@ class LogRepository(private val logDao: LogDao) {
runCatching {
val item = getItem(id) ?: return
val log = item.content ?: ""
val lines = log.split("\n")
//clean dublicate progress + add newline
var newLine = line
if (newLine.contains("[download")){
newLine = "[download]" + line.split("[download]").last()
}
val l = lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}"
item.content = l
item.content = log.appendLineToLog(line)
//item.content += "\n$line"
logDao.update(item)
}

View file

@ -1,8 +1,13 @@
package com.deniscerri.ytdlnis.database.repository
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.dao.ObserveSourcesDao
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver
import kotlinx.coroutines.flow.Flow
class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao) {
@ -57,4 +62,16 @@ class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao)
observeSourcesDao.update(item)
}
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,17 +1,24 @@
package com.deniscerri.ytdlnis.database.repository
import android.content.Context
import android.util.Patterns
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.InfoUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern
class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
private val tag: String = "ResultRepository"
val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1)
enum class SourceType {
YOUTUBE_VIDEO, YOUTUBE_PLAYLIST, SEARCH_QUERY, YT_DLP
}
suspend fun insert(it: ResultItem){
resultDao.insert(it)
}
@ -28,46 +35,52 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
resultDao.insertMultiple(items)
}
suspend fun search(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
suspend fun search(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
val infoUtil = InfoUtil(context)
if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery)
itemCount.value = res.size
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index]?.id = id
if (addToResults){
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index].id = id
}
}
return res
}
suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
val infoUtil = InfoUtil(context)
val v = infoUtil.getYoutubeVideo(inputQuery)
val v = infoUtil.getYoutubeVideo(inputQuery) ?: return arrayListOf()
if (resetResults) {
deleteAll()
itemCount.value = v.size
}else{
v.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it?.playlistTitle = "ytdlnis-Search" }
v.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = "ytdlnis-Search" }
}
val ids = resultDao.insertMultiple(v)
ids.forEachIndexed { index, id ->
v[index]?.id = id
if (addToResults){
val ids = resultDao.insertMultiple(v)
ids.forEachIndexed { index, id ->
v[index].id = id
}
}
return ArrayList(v)
}
suspend fun getPlaylist(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
suspend fun getPlaylist(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
val query = inputQuery.split("list=".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1].split("&").first()
var nextPageToken = ""
if (resetResults) deleteAll()
val infoUtil = InfoUtil(context)
val items : ArrayList<ResultItem?> = arrayListOf()
val items = arrayListOf<ResultItem>()
do {
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0]?.playlistTitle ?: "" else "")
val ids = resultDao.insertMultiple(tmp.videos.toList())
ids.forEachIndexed { index, id ->
tmp.videos[index]?.id = id
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0].playlistTitle else "")
if (addToResults){
val ids = resultDao.insertMultiple(tmp.videos.toList())
ids.forEachIndexed { index, id ->
tmp.videos[index].id = id
}
}
items.addAll(tmp.videos)
val tmpToken = tmp.nextPageToken
@ -79,19 +92,23 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return items
}
suspend fun getDefault(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?> {
suspend fun getDefault(inputQuery: String, resetResults: Boolean, addToResults: Boolean, singleItem: Boolean = false) : ArrayList<ResultItem> {
val infoUtil = InfoUtil(context)
val items = infoUtil.getFromYTDL(inputQuery)
val items = infoUtil.getFromYTDL(inputQuery, singleItem)
if (resetResults) {
deleteAll()
itemCount.value = items.size
}else{
items.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it!!.playlistTitle = "ytdlnis-Search" }
items.filter { it.playlistTitle.isNullOrBlank() }.forEach { it.playlistTitle = "ytdlnis-Search" }
}
val ids = resultDao.insertMultiple(items.toList())
ids.forEachIndexed { index, id ->
items[index]?.id = id
if (addToResults){
val ids = resultDao.insertMultiple(items.toList())
ids.forEachIndexed { index, id ->
items[index].id = id
}
}
return items
}
@ -111,4 +128,59 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
fun getItemByURL(url: String): ResultItem? {
return resultDao.getResultByURL(url)
}
suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : ArrayList<ResultItem> {
return when(getQueryType(inputQuery)){
SourceType.YOUTUBE_VIDEO -> {
getYoutubeVideo(inputQuery, resetResults, addToResults)
}
SourceType.YOUTUBE_PLAYLIST -> {
if (singleItem){
getDefault(inputQuery, resetResults, addToResults, true)
}else{
getPlaylist(inputQuery, resetResults, addToResults)
}
}
SourceType.SEARCH_QUERY -> {
search(inputQuery, resetResults, addToResults)
}
SourceType.YT_DLP -> {
getDefault(inputQuery, resetResults, addToResults)
}
}
}
fun getQueryType(inputQuery: String) : SourceType {
var type = SourceType.SEARCH_QUERY
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery)
if (m.find()) {
type = SourceType.YOUTUBE_VIDEO
if (inputQuery.contains("playlist?list=")) {
type = SourceType.YOUTUBE_PLAYLIST
}
} else if (Patterns.WEB_URL.matcher(inputQuery).matches()) {
type = SourceType.YT_DLP
}
return type
}
suspend fun updateDownloadItem(
downloadItem: DownloadItem
) : DownloadItem? {
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
runCatching {
val info = getResultsFromSource(downloadItem.url, resetResults = false, addToResults = false, singleItem = true).first()
if (downloadItem.title.isEmpty()) downloadItem.title = info.title
if (downloadItem.author.isEmpty()) downloadItem.author = info.author
downloadItem.duration = info.duration
downloadItem.website = info.website
if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info.thumb
return downloadItem
}
}
return null
}
}

View file

@ -17,12 +17,11 @@ import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.repository.CookieRepository
import com.deniscerri.ytdlnis.ui.more.CookiesFragment
import com.deniscerri.ytdlnis.ui.more.WebViewActivity
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.net.URL
import java.util.Date
class CookieViewModel(private val application: Application) : AndroidViewModel(application) {
@ -44,7 +43,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
return repository.getAll()
}
fun getByURL(url: String) : CookieItem {
fun getByURL(url: String) : CookieItem? {
return repository.getByURL(url)
}
@ -75,7 +74,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
}
@SuppressLint("SdCardPath")
fun getCookiesFromDB() : Result<String> = kotlin.runCatching {
fun getCookiesFromDB(url: String) : Result<String> = kotlin.runCatching {
CookieManager.getInstance().run {
if (!hasCookies()) throw Exception("There is no cookies in the database!")
flush()
@ -83,30 +82,35 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
val dbPath = File("/data/data/${BuildConfig.APPLICATION_ID}/").walkTopDown().find { it.name == "Cookies" }
?: throw Exception("Cookies File not found!")
SQLiteDatabase.openDatabase(
val db = SQLiteDatabase.openDatabase(
dbPath.absolutePath, null, OPEN_READONLY
).run {
val projection = arrayOf(
CookieObject.HOST,
CookieObject.EXPIRY,
CookieObject.PATH,
CookieObject.NAME,
CookieObject.VALUE,
CookieObject.SECURE
)
val cookieList = mutableListOf<WebViewActivity.CookieItem>()
query(
"cookies", projection, null, null, null, null, null
).run {
while (moveToNext()) {
val expiry = getLong(getColumnIndexOrThrow(CookieObject.EXPIRY))
val name = getString(getColumnIndexOrThrow(CookieObject.NAME))
val value = getString(getColumnIndexOrThrow(CookieObject.VALUE))
val path = getString(getColumnIndexOrThrow(CookieObject.PATH))
val secure = getLong(getColumnIndexOrThrow(CookieObject.SECURE)) == 1L
val hostKey = getString(getColumnIndexOrThrow(CookieObject.HOST))
)
val host = if (hostKey[0] != '.') ".$hostKey" else hostKey
val projection = arrayOf(
CookieObject.HOST,
CookieObject.EXPIRY,
CookieObject.PATH,
CookieObject.NAME,
CookieObject.VALUE,
CookieObject.SECURE
)
val cookieList = mutableListOf<WebViewActivity.CookieItem>()
db.query(
"cookies", projection, null, null, null, null, null
).run {
while (moveToNext()) {
val expiry = getLong(getColumnIndexOrThrow(CookieObject.EXPIRY))
val name = getString(getColumnIndexOrThrow(CookieObject.NAME))
val value = getString(getColumnIndexOrThrow(CookieObject.VALUE))
val path = getString(getColumnIndexOrThrow(CookieObject.PATH))
val secure = getLong(getColumnIndexOrThrow(CookieObject.SECURE)) == 1L
val hostKey = getString(getColumnIndexOrThrow(CookieObject.HOST))
val host = if (hostKey[0] != '.') ".$hostKey" else hostKey
val requiredHost = URL(url).host
if(host.contains(requiredHost)){
cookieList.add(
WebViewActivity.CookieItem(
domain = host,
@ -118,13 +122,13 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
)
)
}
close()
}
close()
cookieList.fold(StringBuilder("")) { acc, cookie ->
acc.append(cookie.toNetscapeFormat()).append("\n")
}.toString()
}
db.close()
cookieList.fold(StringBuilder("")) { acc, cookie ->
acc.append(cookie.toNetscapeFormat()).append("\n")
}.toString()
}
fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) {

View file

@ -1,36 +1,25 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.Looper
import android.util.DisplayMetrics
import android.widget.Toast
import androidx.compose.animation.core.updateTransition
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.AudioPreferences
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -41,12 +30,11 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.ui.downloadcard.FormatTuple
import com.deniscerri.ytdlnis.util.DownloadUtil
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
@ -59,7 +47,6 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
@ -72,11 +59,16 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItem>>
val activeDownloadsCount : Flow<Int>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
val savedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloadsCount : Flow<Int>
val queuedDownloadsCount : Flow<Int>
val cancelledDownloadsCount : Flow<Int>
val erroredDownloadsCount : Flow<Int>
val savedDownloadsCount : Flow<Int>
private var bestVideoFormat : Format
private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList<Format>
@ -94,6 +86,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private var audioCodec: String?
private val dao: DownloadDao
private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository
data class AlreadyExistsUIState(
var historyItems: MutableList<Long>,
@ -120,15 +113,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao = dbManager.downloadDao
repository = DownloadRepository(dao)
historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application)
activeDownloadsCount = repository.activeDownloadsCount
queuedDownloadsCount = repository.queuedDownloadsCount
cancelledDownloadsCount = repository.cancelledDownloadsCount
erroredDownloadsCount = repository.erroredDownloadsCount
savedDownloadsCount = repository.savedDownloadsCount
allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads
processingDownloads = repository.processingDownloads
activeDownloadsCount = repository.activeDownloadsCount
savedDownloads = repository.savedDownloads.flow
cancelledDownloads = repository.cancelledDownloads.flow
erroredDownloads = repository.erroredDownloads.flow
@ -663,17 +662,17 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items)
DownloadUtil.startDownloadWorker(emptyList(), application)
repository.startDownloadWorker(emptyList(), application)
}
suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{
dbManager.downloadDao.putAtTopOfTheQueue(id)
DownloadUtil.startDownloadWorker(emptyList(), application)
suspend fun putAtTopOfQueue(ids: List<Long>) = CoroutineScope(Dispatchers.IO).launch{
dbManager.downloadDao.putAtTopOfTheQueue(ids)
repository.startDownloadWorker(emptyList(), application)
}
suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.reQueueDownloadItems(items)
DownloadUtil.startDownloadWorker(emptyList(), application)
repository.startDownloadWorker(emptyList(), application)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : Pair<List<Long>, List<Long>> {
@ -761,19 +760,27 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
if (queuedItems.isNotEmpty()){
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime()){
DownloadUtil.startDownloadWorker(queuedItems, context)
repository.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){}
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
}catch (ignored: Exception){}
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
}
@ -819,7 +826,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
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)
repository.update(i)
}
return items.map { it.format.filesize }
@ -829,7 +836,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val items = repository.getProcessingDownloads()
items.forEach { i ->
i.format = format
updateDownload(i)
repository.update(i)
}
}
@ -837,7 +844,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val items = repository.getProcessingDownloads()
items.forEach { i ->
i.downloadPath = path
updateDownload(i)
repository.update(i)
}
}
@ -866,7 +873,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dbManager.resultDao.update(this)
}
}
updateDownload(i)
repository.update(i)
}
}
@ -874,7 +881,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.getProcessingDownloadsList().apply {
this.forEach {
it.status = DownloadRepository.Status.Saved.toString()
updateDownload(it)
repository.update(it)
}
val ids = this.map { it.id }
@ -900,7 +907,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.getProcessingDownloads().apply {
val new = switchDownloadType(this, newType)
new.forEach {
updateDownload(it)
repository.update(it)
}
}
}
@ -933,28 +940,5 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getURLsByStatus(list.map { it.toString() })
}
private fun updateDownloadItem(
downloadItem: DownloadItem,
infoUtil: InfoUtil,
dao: DownloadDao,
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

@ -8,9 +8,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySortType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View file

@ -3,7 +3,6 @@ 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
@ -12,12 +11,9 @@ 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
@ -63,13 +59,13 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
}
fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) {
runCatching { DownloadUtil.cancelObservationTaskByID(application, item.id) }
runCatching { repository.cancelObservationTaskByID(application, item.id) }
repository.delete(item)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
getAll().forEach {
runCatching { DownloadUtil.cancelObservationTaskByID(application, it.id) }
runCatching { repository.cancelObservationTaskByID(application, it.id) }
}
repository.deleteAll()
@ -94,7 +90,7 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
c.set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
DownloadUtil.cancelObservationTaskByID(application, id)
repository.cancelObservationTaskByID(application, id)
val intent = Intent(application, ObserveAlarmReceiver::class.java)
intent.putExtra("id", id)

View file

@ -5,7 +5,6 @@ import android.app.ActivityManager.RunningAppProcessInfo
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import android.util.Patterns
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
@ -21,14 +20,14 @@ import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.regex.Pattern
import java.util.concurrent.CancellationException
class ResultViewModel(private val application: Application) : AndroidViewModel(application) {
@ -53,6 +52,18 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
errorMessage = null,
actions = null
))
val updatingData: MutableStateFlow<Boolean> = MutableStateFlow(false)
var updateResultData: MutableStateFlow<List<ResultItem?>?> = MutableStateFlow(null)
private var updateResultDataJob : Job? = null
val updatingFormats: MutableStateFlow<Boolean> = MutableStateFlow(false)
var updateFormatsResultData: MutableStateFlow<MutableList<Format>?> = MutableStateFlow(null)
private var updateFormatsResultDataJob: Job? = null
var parsingQueries: Job? = null
private val sharedPreferences: SharedPreferences
init {
@ -86,41 +97,45 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
deleteAll()
}
}
suspend fun parseQueries(inputQueries: List<String>) : List<ResultItem?> {
if (inputQueries.size > 1){
repository.itemCount.value = inputQueries.size
}
val resetResults = inputQueries.size == 1
uiState.update {it.copy(processing = true, errorMessage = null, actions = null)}
return withContext(Dispatchers.IO){
val res = mutableListOf<ResultItem?>()
inputQueries.forEach { inputQuery ->
val type = getQueryType(inputQuery)
try {
when (type) {
"Search" -> res.addAll(repository.search(inputQuery, resetResults))
"YT_Video" -> res.addAll(repository.getYoutubeVideo(inputQuery, resetResults))
"YT_Playlist" -> res.addAll(repository.getPlaylist(inputQuery, resetResults))
else -> res.addAll(repository.getDefault(inputQuery, resetResults))
}
} catch (e: Exception) {
if (updateResultDataJob?.isCancelled == false){
uiState.update {it.copy(
processing = false,
errorMessage = Pair(R.string.no_results, e.message.toString()),
actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG))
)}
Log.e(tag, e.toString())
}
fun cancelParsingQueries(){
parsingQueries?.cancel(CancellationException())
uiState.update {it.copy(processing = false)}
}
suspend fun parseQueries(inputQueries: List<String>, onResult: (list: List<ResultItem?>) -> Unit) {
if (parsingQueries == null || parsingQueries?.isCancelled == true || parsingQueries?.isCompleted == true) {
parsingQueries = viewModelScope.launch(Dispatchers.IO) {
if (inputQueries.size > 1){
repository.itemCount.value = inputQueries.size
}
val resetResults = inputQueries.size == 1
uiState.update {it.copy(processing = true, errorMessage = null, actions = null)}
val res = mutableListOf<ResultItem?>()
inputQueries.forEach { inputQuery ->
try {
res.addAll(repository.getResultsFromSource(inputQuery, resetResults))
} catch (e: Exception) {
if (updateResultDataJob?.isCancelled == false || e is YoutubeDLException){
uiState.update {it.copy(
processing = false,
errorMessage = Pair(R.string.no_results, e.message.toString()),
actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG))
)}
Log.e(tag, e.toString())
}
}
}
if (!isForegrounded() && inputQueries.size > 1){
notificationUtil.showQueriesFinished()
}
uiState.update {it.copy(processing = false)}
onResult(res)
}
if (!isForegrounded() && inputQueries.size > 1){
notificationUtil.showQueriesFinished()
}
uiState.update {it.copy(processing = false)}
res
}else{
onResult(listOf())
}
}
@ -133,20 +148,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
}.getOrElse { true }
}
private fun getQueryType(inputQuery: String) : String {
var type = "Search"
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery)
if (m.find()) {
type = "YT_Video"
if (inputQuery.contains("playlist?list=")) {
type = "YT_Playlist"
}
} else if (Patterns.WEB_URL.matcher(inputQuery).matches()) {
type = "Default"
}
return type
}
fun insert(items: ArrayList<ResultItem?>) = viewModelScope.launch(Dispatchers.IO){
items.forEach {
@ -196,21 +198,19 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
}
}
val updatingData: MutableStateFlow<Boolean> = MutableStateFlow(false)
var updateResultData: MutableStateFlow<List<ResultItem?>?> = MutableStateFlow(null)
private var updateResultDataJob : Job? = null
val updatingFormats: MutableStateFlow<Boolean> = MutableStateFlow(false)
var updateFormatsResultData: MutableStateFlow<MutableList<Format>?> = MutableStateFlow(null)
private var updateFormatsResultDataJob: Job? = null
suspend fun updateItemData(res: ResultItem){
if (updateResultDataJob == null || updateResultDataJob?.isCancelled == true || updateResultDataJob?.isCompleted == true){
updateResultDataJob = viewModelScope.launch(Dispatchers.IO) {
updatingData.emit(true)
val result = parseQueries(listOf(res.url))
updatingData.emit(false)
updateResultData.emit(result)
updatingData.value = true
parseQueries(listOf(res.url)){ result ->
viewModelScope.launch(Dispatchers.IO){
updatingData.emit(false)
updatingData.value = false
updateResultData.emit(result)
}
}
}
updateResultDataJob?.start()
@ -218,6 +218,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
if (it != null){
viewModelScope.launch(Dispatchers.IO) {
updatingData.emit(false)
updatingData.value = false
updateResultData.emit(mutableListOf())
}
}
@ -226,13 +227,14 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
}
suspend fun cancelUpdateItemData(){
updateResultDataJob?.cancel()
updateResultDataJob?.cancel(CancellationException())
updatingData.emit(false)
updatingData.value = false
updateResultData.emit(null)
}
suspend fun cancelUpdateFormatsItemData(){
updateFormatsResultDataJob?.cancel()
updateFormatsResultDataJob?.cancel(CancellationException())
updatingFormats.emit(false)
updateFormatsResultData.emit(null)
}

View file

@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.NotificationUtil

View file

@ -8,7 +8,6 @@ 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

View file

@ -10,9 +10,11 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PauseDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
val result = goAsync()
val id = intent.getIntExtra("itemID", 0)
if (id != 0) {
runCatching {
@ -20,13 +22,17 @@ class PauseDownloadNotificationReceiver : BroadcastReceiver() {
val notificationUtil = NotificationUtil(c)
notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.createResumeDownload(id, title)
val dbManager = DBManager.getInstance(c)
CoroutineScope(Dispatchers.IO).launch{
runCatching {
try {
val item = dbManager.downloadDao.getDownloadById(id.toLong())
item.status = DownloadRepository.Status.ActivePaused.toString()
dbManager.downloadDao.update(item)
}finally {
withContext(Dispatchers.Main){
notificationUtil.createResumeDownload(id, title)
result.finish()
}
}
}
}

View file

@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.receiver.downloadAlarmReceivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder

View file

@ -1,6 +1,5 @@
package com.deniscerri.ytdlnis.ui
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
@ -15,14 +14,12 @@ import android.util.Log
import android.util.Patterns
import android.view.*
import android.view.View.*
import android.view.animation.Interpolator
import android.widget.*
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
@ -45,7 +42,6 @@ import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter
import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdlnis.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
@ -220,7 +216,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
if (inputQueries != null) {
resultViewModel.deleteAll()
lifecycleScope.launch(Dispatchers.IO){
resultViewModel.parseQueries(inputQueries!!)
resultViewModel.parseQueries(inputQueries!!){}
inputQueries = null
}
}
@ -321,11 +317,25 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
requireView().post {
checkClipboard().apply {
clipboardFab?.isVisible = this != null
clipboardFab?.setOnClickListener {
searchView!!.setText(this)
clipboardFab?.isVisible = false
initSearch(searchView!!)
this?.apply {
clipboardFab?.isVisible = this.isNotEmpty()
clipboardFab?.setOnClickListener {
if (this.size == 1){
searchView?.setText(this.first())
clipboardFab?.isVisible = false
initSearch(searchView!!)
}else{
searchBar?.performClick()
lifecycleScope.launch {
withContext(Dispatchers.IO){
delay(500)
}
this@apply.forEach {
onSearchSuggestionAdd(it)
}
}
}
}
}
lifecycleScope.launch {
@ -435,6 +445,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
R.id.delete_results -> {
resultViewModel.cancelParsingQueries()
resultViewModel.getTrending()
selectedObjects = ArrayList()
searchBar!!.setText("")
@ -489,7 +500,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val url = checkClipboard()
url?.apply {
combinedList.add(0, SearchSuggestionItem(this, SearchSuggestionType.CLIPBOARD))
combinedList.add(0, SearchSuggestionItem(this.joinToString("\n"), SearchSuggestionType.CLIPBOARD))
}
@ -614,10 +625,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
}else{
resultViewModel.parseQueries(queryList)
resultViewModel.parseQueries(queryList){}
}
}else{
resultViewModel.parseQueries(queryList)
resultViewModel.parseQueries(queryList){}
}
}
}
@ -830,12 +841,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
private fun checkClipboard(): String?{
private fun checkClipboard(): List<String>?{
return kotlin.runCatching {
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clip = clipboard.primaryClip!!.getItemAt(0).text
val matchesURL = Patterns.WEB_URL.matcher(clip.toString()).matches()
return if (matchesURL) clip.toString() else null
return clip.split("\r\n").filter { Patterns.WEB_URL.matcher(it).matches() }
}.getOrNull()
}
@ -850,9 +860,17 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
override fun onSearchSuggestionClick(text: String) {
clipboardFab?.isVisible = false
searchView!!.setText(text)
initSearch(searchView!!)
val res = text.split("\n")
if (res.size == 1){
clipboardFab?.isVisible = false
searchView!!.setText(text)
initSearch(searchView!!)
}else{
res.forEach {
onSearchSuggestionAdd(it)
}
}
}
override fun onSearchSuggestionAdd(text: String) {

View file

@ -10,7 +10,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
@ -22,9 +21,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.dp
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -63,7 +60,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.popup()
card.tag = "${item!!.id}##card"
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.image_view)
@ -98,7 +94,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
// Author ----------------------------------
val author = card.findViewById<TextView>(R.id.author)
var info = item.author
if (item.duration.isNotEmpty()) {
if (item.duration.isNotEmpty() && item.duration != "-1") {
if (item.author.isNotEmpty()) info += ""
info += item.duration
}
@ -153,6 +149,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
val value = animation.animatedValue as Int
pauseButton.cornerRadius = value
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white)
pauseButton.contentDescription = activity.getString(R.string.pause)
pauseButton.isEnabled = true
pauseButton.tag = ActiveDownloadAction.Pause
}
@ -171,6 +168,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
val value = animation.animatedValue as Int
pauseButton.cornerRadius = value
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white)
pauseButton.contentDescription = activity.getString(R.string.start)
pauseButton.tag = ActiveDownloadAction.Resume
pauseButton.isEnabled = true
}
@ -180,6 +178,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
DownloadRepository.Status.PausedReQueued -> {
progressBar.isIndeterminate = true
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.ic_refresh)
pauseButton.contentDescription = activity.getString(R.string.please_wait)
pauseButton.tag = null
pauseButton.isEnabled = false
cancelButton.visibility = View.GONE

View file

@ -25,7 +25,6 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.squareup.picasso.Picasso
import java.lang.StringBuilder
class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadMinifiedAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
@ -122,11 +121,13 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
if (item.status == DownloadRepository.Status.ActivePaused.toString()){
progressBar.isIndeterminate = false
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white)
pauseButton.contentDescription = activity.getString(R.string.start)
pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Resume
cancelButton.visibility = View.VISIBLE
}else{
progressBar.isIndeterminate = true
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white)
pauseButton.contentDescription = activity.getString(R.string.pause)
cancelButton.visibility = View.GONE
pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Pause
}

View file

@ -5,7 +5,6 @@ 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
@ -13,16 +12,10 @@ 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

View file

@ -130,12 +130,15 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
when(item.type) {
DownloadViewModel.Type.audio -> {
btn.setIconResource(R.drawable.ic_music)
btn.contentDescription = activity.getString(R.string.audio)
}
DownloadViewModel.Type.video -> {
btn.setIconResource(R.drawable.ic_video)
btn.contentDescription = activity.getString(R.string.video)
}
else -> {
btn.setIconResource(R.drawable.ic_terminal)
btn.contentDescription = activity.getString(R.string.command)
}
}

View file

@ -63,9 +63,18 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
val downloadTypeIcon = card.findViewById<MaterialButton>(R.id.download_type)
when(item.downloadType){
DownloadViewModel.Type.audio -> downloadTypeIcon.setIconResource(R.drawable.ic_music)
DownloadViewModel.Type.video -> downloadTypeIcon.setIconResource(R.drawable.ic_video)
DownloadViewModel.Type.command -> downloadTypeIcon.setIconResource(R.drawable.ic_terminal)
DownloadViewModel.Type.audio -> {
downloadTypeIcon.setIconResource(R.drawable.ic_music)
downloadTypeIcon.contentDescription = activity.getString(R.string.audio)
}
DownloadViewModel.Type.video -> {
downloadTypeIcon.setIconResource(R.drawable.ic_video)
downloadTypeIcon.contentDescription = activity.getString(R.string.video)
}
DownloadViewModel.Type.command -> {
downloadTypeIcon.setIconResource(R.drawable.ic_terminal)
downloadTypeIcon.contentDescription = activity.getString(R.string.command)
}
else -> {}
}

View file

@ -118,11 +118,21 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
if (actionButton.hasOnClickListeners()) actionButton.setOnClickListener(null)
when(item.status){
DownloadRepository.Status.Cancelled.toString() -> actionButton.setIconResource(R.drawable.ic_refresh)
DownloadRepository.Status.Saved.toString() -> actionButton.setIconResource(R.drawable.ic_downloads)
DownloadRepository.Status.Queued.toString() -> actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24)
DownloadRepository.Status.Cancelled.toString() -> {
actionButton.setIconResource(R.drawable.ic_refresh)
actionButton.contentDescription = activity.getString(R.string.download)
}
DownloadRepository.Status.Saved.toString() -> {
actionButton.setIconResource(R.drawable.ic_downloads)
actionButton.contentDescription = activity.getString(R.string.download)
}
DownloadRepository.Status.Queued.toString() -> {
actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24)
actionButton.contentDescription = activity.getString(R.string.Remove)
}
else -> {
actionButton.setIconResource(R.drawable.ic_baseline_file_open_24)
actionButton.contentDescription = activity.getString(R.string.logs)
if (item.logID == null){
actionButton.visibility = View.GONE
}

View file

@ -114,7 +114,7 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
author.text = item.author
val length = card.findViewById<TextView>(R.id.length)
length.text = if(item.downloadPath.size == 1) item.duration else "[${item.downloadPath.size}]"
length.text = if(item.downloadPath.size == 1) item.duration else ""
// TIME DOWNLOADED ----------------------------------

View file

@ -1,9 +1,7 @@
package com.deniscerri.ytdlnis.ui.adapter
import android.animation.ValueAnimator
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
@ -19,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -72,7 +69,6 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
} else {
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
}
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
}else{
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
}

View file

@ -1,11 +1,9 @@
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
@ -25,7 +23,6 @@ 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
@ -139,7 +136,6 @@ class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity:
}
}else{
info.isVisible = true
checkMissing.isVisible = true
btn.setImageResource(R.drawable.ic_search)
btn.setOnClickListener {

View file

@ -2,7 +2,6 @@ package com.deniscerri.ytdlnis.ui.adapter
import android.app.Activity
import android.content.SharedPreferences
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

View file

@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.ui.util.fastJoinToString
import androidx.core.view.isVisible
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
@ -14,7 +13,6 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView

View file

@ -19,9 +19,9 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.Dispatchers

View file

@ -19,8 +19,6 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
@ -36,7 +34,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
//TODO MAKE CONFIGURE LIKE NORMAL SHEET
class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private val currentDownloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2
@ -77,6 +74,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager)
viewPager2.isUserInputEnabled = sharedPreferences.getBoolean("swipe_gestures_download_card", true)
(viewPager2.getChildAt(0) as? RecyclerView)?.apply {
isNestedScrollingEnabled = false
@ -231,6 +229,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
}
}
1 -> {

View file

@ -28,10 +28,10 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment

View file

@ -15,7 +15,6 @@ import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
@ -23,7 +22,6 @@ import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.Log
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
@ -37,14 +35,13 @@ import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@ -83,7 +80,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null){
@ -304,12 +300,22 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
updateDataClicked = {
CoroutineScope(Dispatchers.IO).launch {
resultItem?.apply {
val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
rsVM.updateItemData(this)
cutDisabledClicked = {
val isUpdatingData = ViewModelProvider(requireActivity())[ResultViewModel::class.java].updatingData.value
if(isUpdatingData){
val snack = Snackbar.make(view, context.getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
snack.show()
}else{
val snack = Snackbar.make(view, context.getString(R.string.cut_unavailable), Snackbar.LENGTH_SHORT)
snack.setAction(R.string.update){
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
resultItem?.apply {
val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
rsVM.updateItemData(this)
}
}
}
snack.show()
}
},
extraCommandsClicked = {
@ -328,6 +334,16 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}catch (e : Exception){
e.printStackTrace()
}
super.onViewCreated(view, savedInstanceState)
}
}
@SuppressLint("RestrictedApi")
fun updateSelectedAudioFormat(formatID: String){
resultItem?.formats?.find { it.format_id == formatID }?.apply {
downloadItem.format = this
val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, listOf())
}
}

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.Configuration
@ -19,7 +20,11 @@ import android.widget.TextView
import android.widget.Toast
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commitNow
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@ -139,6 +144,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager)
updateItem = view.findViewById(R.id.update_item)
viewPager2.isUserInputEnabled = sharedPreferences.getBoolean("swipe_gestures_download_card", true)
//loading shimmers
@ -307,12 +313,31 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
download.isEnabled = false
val item: DownloadItem = getDownloadItem()
item.downloadStartTime = it.timeInMillis
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
if (item.videoPreferences.alsoDownloadAsAudio){
val itemsToQueue = mutableListOf<DownloadItem>()
itemsToQueue.add(item)
getAlsoAudioDownloadItem{ audioDownloadItem ->
audioDownloadItem.downloadStartTime = it.timeInMillis
itemsToQueue.add(audioDownloadItem)
runBlocking {
downloadViewModel.queueDownloads(itemsToQueue)
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}
dismiss()
}
}else{
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}
dismiss()
}
dismiss()
}
}
download!!.setOnClickListener {
@ -324,10 +349,24 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
scheduleBtn.isEnabled = false
download.isEnabled = false
val item: DownloadItem = getDownloadItem()
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
if (item.videoPreferences.alsoDownloadAsAudio){
val itemsToQueue = mutableListOf<DownloadItem>()
itemsToQueue.add(item)
getAlsoAudioDownloadItem {
itemsToQueue.add(it)
runBlocking {
downloadViewModel.queueDownloads(itemsToQueue)
}
dismiss()
}
}else{
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
dismiss()
}
dismiss()
}
}
@ -563,7 +602,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem {
return when(selectedTabPosition){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
@ -580,6 +619,33 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
private fun getAlsoAudioDownloadItem(finished: (it: DownloadItem) -> Unit) {
try {
val ff = (fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment)
ff.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
finished(ff.downloadItem)
}catch (e: Exception){
val fragmentLifecycleCallback = object:
FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
fragmentManager?.unregisterFragmentLifecycleCallbacks(this)
val ff = (f as DownloadAudioFragment)
ff.requireView().post {
ff.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
finished(ff.downloadItem)
}
super.onFragmentStarted(fm, f)
}
}
fragmentManager?.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback, true)
viewPager2.setCurrentItem(0, true)
}
}
private fun updateWhenSwitching(){
val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1
@ -591,6 +657,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
}
}
1 -> {

View file

@ -30,17 +30,15 @@ import com.deniscerri.ytdlnis.database.models.Format
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.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.UiUtil.populateCommandCard
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File

View file

@ -10,8 +10,6 @@ import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.view.LayoutInflater
@ -30,15 +28,10 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
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.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
@ -48,8 +41,6 @@ import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -63,7 +54,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
@ -80,6 +70,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView
private lateinit var count : TextView
private lateinit var sharedPreferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
@ -135,6 +126,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count)
scheduleBtn.setOnClickListener{
@ -406,7 +398,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomSheet.dismiss()
},
cutClicked = {},
updateDataClicked = {},
cutDisabledClicked = {},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
@ -476,7 +468,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomSheet.dismiss()
},
cutClicked = {},
updateDataClicked = {},
cutDisabledClicked = {},
filenameTemplateSet = { checked ->
items.forEach { it.customFileNameTemplate = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
@ -497,6 +489,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
items.forEach { it.videoPreferences.removeAudio = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
alsoDownloadAsAudioClicked = {},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
@ -536,6 +529,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest {
count.text = "${it.size} ${getString(R.string.selected)}"
listAdapter.submitList(it)
withContext(Dispatchers.Main){
updateFileSize(it.map { it2 -> it2.format.filesize })
@ -575,7 +569,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
}
private fun updateFileSize(items: List<Long>){

View file

@ -35,6 +35,7 @@ import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM
import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
@ -322,12 +323,22 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
updateDataClicked = {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
resultItem?.apply {
val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
rsVM.updateItemData(this)
cutDisabledClicked = {
val isUpdatingData = ViewModelProvider(requireActivity())[ResultViewModel::class.java].updatingData.value
if(isUpdatingData){
val snack = Snackbar.make(view, context.getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
snack.show()
}else{
val snack = Snackbar.make(view, context.getString(R.string.cut_unavailable), Snackbar.LENGTH_SHORT)
snack.setAction(R.string.update){
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
resultItem?.apply {
val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
rsVM.updateItemData(this)
}
}
}
snack.show()
}
},
filenameTemplateSet = {
@ -346,6 +357,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
downloadItem.videoPreferences.removeAudio = it
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, if (it) listOf() else downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
},
alsoDownloadAsAudioClicked = {
downloadItem.videoPreferences.alsoDownloadAsAudio = it
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {

View file

@ -16,10 +16,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
@ -30,8 +28,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job

View file

@ -7,8 +7,6 @@ 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
@ -16,23 +14,20 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.LinearLayout
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
@ -49,7 +44,6 @@ 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
@ -58,12 +52,9 @@ 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
@ -102,6 +93,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var endsOnTime: TextInputLayout
private lateinit var endsAfterNr: TextInputLayout
private lateinit var retryMissingDownloads: MaterialSwitch
private lateinit var getOnlyNewUploads: MaterialSwitch
override fun onCreate(savedInstanceState: Bundle?) {
@ -114,13 +106,13 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
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")
}
type = currentItem?.downloadItemTemplate?.type ?: arguments?.getSerializable("type") as Type
}
override fun onSaveInstanceState(outState: Bundle) {
@ -259,6 +251,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfter = view.findViewById(R.id.after)
endsAfterNr = view.findViewById(R.id.after_nr)
retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads)
getOnlyNewUploads = view.findViewById(R.id.get_new_uploads)
okButton = view.findViewById(R.id.okButton)
title.editText!!.setText(currentItem?.name ?: "")
@ -451,6 +444,9 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfter.performClick()
}
retryMissingDownloads.isChecked = currentItem?.retryMissingDownloads ?: false
getOnlyNewUploads.isChecked = currentItem?.getOnlyNewUploads ?: false
if (currentItem != null) okButton.text = getString(R.string.update)
okButton.setOnClickListener {
lifecycleScope.launch {
@ -483,6 +479,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
ends,
endsAfterCount,
0,
getOnlyNewUploads.isChecked,
retryMissingDownloads.isChecked,
mutableListOf()
)

View file

@ -19,13 +19,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.widget.*
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
@ -39,13 +39,13 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.ui.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.ui.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.util.Extensions.setFullScreen
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
@ -71,7 +71,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*
import java.util.Calendar
import java.util.Locale
class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener {

View file

@ -2,7 +2,6 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.ActionBar.LayoutParams
import android.app.Dialog
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
@ -19,13 +18,11 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.PlaylistAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.ui.adapter.PlaylistAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.google.android.material.appbar.MaterialToolbar

View file

@ -18,12 +18,12 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL

View file

@ -27,14 +27,13 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder

View file

@ -1,13 +1,9 @@
package com.deniscerri.ytdlnis.ui.downloads
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadAudioFragment
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadCommandFragment
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadVideoFragment
class DownloadListFragmentAdapter (
fragmentManager : FragmentManager,

View file

@ -1,18 +1,18 @@
package com.deniscerri.ytdlnis.ui.downloads
import android.content.DialogInterface
import android.content.SharedPreferences
import android.icu.text.Transliterator.Position
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import androidx.work.WorkManager
@ -20,14 +20,17 @@ import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.createBadge
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -40,6 +43,7 @@ class DownloadQueueMainFragment : Fragment(){
private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadListFragmentAdapter
private lateinit var mainActivity: MainActivity
private lateinit var sharedPreferences: SharedPreferences
override fun onCreateView(
inflater: LayoutInflater,
@ -47,13 +51,14 @@ class DownloadQueueMainFragment : Fragment(){
savedInstanceState: Bundle?
): View? {
mainActivity = activity as MainActivity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
return inflater.inflate(R.layout.fragment_download_queue_main_screen, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
workManager = WorkManager.getInstance(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
topAppBar = view.findViewById(R.id.logs_toolbar)
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
@ -113,6 +118,50 @@ class DownloadQueueMainFragment : Fragment(){
viewPager2.setCurrentItem(3, false)
}, 200)
}
if (sharedPreferences.getBoolean("show_count_downloads", false)){
lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest {
tabLayout.getTabAt(0)?.apply {
if (it == 0) removeBadge()
else createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.queuedDownloadsCount.collectLatest {
tabLayout.getTabAt(1)?.apply {
if (it == 0) removeBadge()
else createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(2)?.apply {
if (it == 0) removeBadge()
else createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest {
tabLayout.getTabAt(3)?.apply {
if (it == 0) removeBadge()
else createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply {
if (it == 0) removeBadge()
else createBadge(it)
}
}
}
}
}
private fun initMenu() {

View file

@ -28,14 +28,13 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder

View file

@ -27,8 +27,6 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.forEach
@ -43,16 +41,14 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.HistoryAdapter
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.ui.adapter.HistoryAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog

View file

@ -28,16 +28,15 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -201,7 +200,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems, adapter.inverted, now)
}
actionMode!!.menu.getItem(1).isVisible = selectedObjects == 1 && position > 0
actionMode!!.menu.getItem(1).isVisible = position > 0
}
}
}
@ -314,22 +313,20 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
R.id.up -> {
lifecycleScope.launch {
if (adapter.getSelectedObjectsCount(totalSize) == 1){
val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused))
}
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
val selectedObjects = if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.putAtTopOfQueue(selectedObjects[0])
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems, listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused))
}
actionMode?.finish()
}else{
adapter.checkedItems.toList()
}
adapter.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtTopOfQueue(selectedObjects)
}
actionMode?.finish()
}
true
}

View file

@ -27,14 +27,13 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder

View file

@ -32,12 +32,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.TemplatesAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository.CommandTemplateSortType
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository.CommandTemplateSortType
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.ui.adapter.TemplatesAdapter
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.content.Context.INPUT_METHOD_SERVICE
import android.content.DialogInterface
import android.content.Intent
@ -63,6 +64,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
return inflater.inflate(R.layout.fragment_cookies, container, false)
}
@SuppressLint("RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
topAppBar = view.findViewById(R.id.logs_toolbar)
@ -164,7 +166,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
private fun showDialog(url: String){
lifecycleScope.launch {
var item = withContext(Dispatchers.IO){
val item = withContext(Dispatchers.IO){
cookiesViewModel.getByURL(url)
}
@ -178,11 +180,15 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
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())
item?.apply {
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 ->

View file

@ -4,23 +4,24 @@ 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 android.widget.TextView
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.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
@ -33,8 +34,8 @@ 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.progressindicator.LinearProgressIndicator
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
@ -73,13 +74,8 @@ class ObserveSourcesFragment : Fragment(), ObserveSourcesAdapter.OnItemClickList
)
val newSource = view.findViewById<Chip>(R.id.newSource)
recyclerView = view.findViewById(R.id.source_recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
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]
@ -163,63 +159,4 @@ class ObserveSourcesFragment : Fragment(), ObserveSourcesAdapter.OnItemClickList
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

@ -24,6 +24,7 @@ import com.google.accompanist.web.AccompanistWebChromeClient
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.google.android.exoplayer2.util.Log
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
@ -57,48 +58,58 @@ class WebViewActivity : BaseActivity() {
cookieManager = CookieManager.getInstance()
preferences = PreferenceManager.getDefaultSharedPreferences(this)
webViewClient = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
kotlin.runCatching {
toolbar.title = view?.title ?: ""
cookies = CookieManager.getInstance().getCookie(view?.url)
}
}
}
cookieManager = CookieManager.getInstance()
toolbar.setNavigationOnClickListener {
cookieManager.flush()
onBackPressedDispatcher.onBackPressed()
}
generateBtn.setOnClickListener {
cookiesViewModel.getCookiesFromDB().getOrNull()?.let {
kotlin.runCatching {
lifecycleScope.launch {
withContext(Dispatchers.IO){
cookiesViewModel.insert(
CookieItem(
0,
url,
it
)
)
}
cookiesViewModel.updateCookiesFile()
lifecycleScope.launch{
webViewClient = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
kotlin.runCatching {
toolbar.title = view?.title ?: ""
cookies = CookieManager.getInstance().getCookie(view?.url)
}
}.onFailure {
Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show()
}
}
onBackPressedDispatcher.onBackPressed()
cookieManager = CookieManager.getInstance()
toolbar.setNavigationOnClickListener {
cookieManager.flush()
onBackPressedDispatcher.onBackPressed()
}
generateBtn.setOnClickListener {
lifecycleScope.launch {
withContext(Dispatchers.IO){
val a = cookieManager.getCookie(url).replace(";", "\n")
Log.e("TESTING", a)
cookiesViewModel.getCookiesFromDB(url).getOrNull()?.let {
kotlin.runCatching {
cookiesViewModel.insert(
CookieItem(
0,
url,
it
)
)
cookiesViewModel.updateCookiesFile()
}.onFailure {
withContext(Dispatchers.Main){
Toast.makeText(this@WebViewActivity, "Something went wrong", Toast.LENGTH_SHORT).show()
}
}
}
withContext(Dispatchers.Main){
onBackPressedDispatcher.onBackPressed()
}
}
}
}
webViewCompose.apply {
setContent { WebViewView() }
}
}
webViewCompose.apply {
setContent { WebViewView() }
}
}
@SuppressLint("SetJavaScriptEnabled")

View file

@ -15,6 +15,7 @@ import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.core.content.edit
import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
@ -168,10 +169,12 @@ class DownloadLogFragment : Fragment() {
requireActivity().runOnUiThread{
if (logItem != null){
if (logItem.content.isNotBlank()) {
content.text = logItem.content
content.setText(logItem.content, TextView.BufferType.SPANNABLE)
}
if (!bottomAppBar.menu.children.first { it.itemId == R.id.scroll_down }.isVisible){
content.scrollTo(0, content.height)
contentScrollView.fullScroll(View.FOCUS_DOWN)
}
content.scrollTo(0, content.height)
contentScrollView.fullScroll(View.FOCUS_DOWN)
}
}
}

View file

@ -23,9 +23,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.DownloadLogsAdapter
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
import com.deniscerri.ytdlnis.ui.adapter.DownloadLogsAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.google.android.material.appbar.MaterialToolbar

View file

@ -1,6 +1,5 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.content.DialogInterface
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference

View file

@ -9,7 +9,6 @@ import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.work.ExistingWorkPolicy

View file

@ -31,14 +31,15 @@ import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
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.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -70,6 +71,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var observeSourcesViewModel: ObserveSourcesViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -127,6 +129,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
observeSourcesViewModel = ViewModelProvider(this)[ObserveSourcesViewModel::class.java]
backup = findPreference("backup")
restore = findPreference("restore")
@ -175,6 +178,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
"templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() )
"searchHistory" -> json.add("search_history", backupSearchHistory() )
"observeSources" -> json.add("observe_sources", backupObserveSources() )
}
}.onFailure {err ->
withContext(Dispatchers.Main){
@ -374,6 +378,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
return JsonArray()
}
private suspend fun backupObserveSources() : JsonArray {
runCatching {
val observeSourcesItems = withContext(Dispatchers.IO) {
observeSourcesViewModel.getAll()
}
val arr = JsonArray()
observeSourcesItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private var appRestoreResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
@ -400,204 +418,263 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
}
val json = Gson().fromJson(total.toString(), JsonObject::class.java)
val finalMessage = StringBuilder("${getString(R.string.restore_complete)}\n")
val finalMessage = StringBuilder("")
val errorMessage = StringBuilder("")
//preference restore
if(json.has("settings")){
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
val prefs = json.getAsJsonArray("settings")
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
kotlin.runCatching {
if(json.has("settings")){
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
val prefs = json.getAsJsonArray("settings")
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
finalMessage.append("${getString(R.string.settings)}: ${prefs.count()}\n")
}
finalMessage.append("${getString(R.string.settings)}: ${prefs.count()}\n")
}
}.onFailure {
errorMessage.append("\n PREFERENCE RESTORE FAILED: \n ${it.message}")
}
//history restore
if(json.has("downloads")){
val items = json.getAsJsonArray("downloads")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
historyViewModel.insert(item)
kotlin.runCatching {
if(json.has("downloads")){
val items = json.getAsJsonArray("downloads")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
historyViewModel.insert(item)
}
}
finalMessage.append("${getString(R.string.downloads)}: ${items.count()}\n")
}
finalMessage.append("${getString(R.string.downloads)}: ${items.count()}\n")
}.onFailure {
errorMessage.append("\n HISTORY RESTORE FAILED: \n ${it.message}")
}
//queued downloads restore
if(json.has("queued")){
val items = json.getAsJsonArray("queued")
val queued = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
queued.add(item)
}
if (queued.isNotEmpty()) {
finalMessage.append("${getString(R.string.in_queue)}: ${queued.count()}\n")
withContext(Dispatchers.IO){downloadViewModel.queueDownloads(queued)}
kotlin.runCatching {
if(json.has("queued")){
val items = json.getAsJsonArray("queued")
val queued = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
queued.add(item)
}
if (queued.isNotEmpty()) {
finalMessage.append("${getString(R.string.in_queue)}: ${queued.count()}\n")
withContext(Dispatchers.IO){downloadViewModel.queueDownloads(queued)}
}
}
}.onFailure {
errorMessage.append("\n QUEUED DOWNLOADS RESTORE FAILED: \n ${it.message}")
}
//cancelled downloads restore
if(json.has("cancelled")){
val items = json.getAsJsonArray("cancelled")
val cancelled = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
cancelled.add(item)
kotlin.runCatching {
if(json.has("cancelled")){
val items = json.getAsJsonArray("cancelled")
val cancelled = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
cancelled.add(item)
}
}
cancelled.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
cancelled.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(cancelled.isNotEmpty()){
finalMessage.append("${getString(R.string.cancelled)}: ${cancelled.count()}\n")
}
}
if(cancelled.isNotEmpty()){
finalMessage.append("${getString(R.string.cancelled)}: ${cancelled.count()}\n")
}
}.onFailure {
errorMessage.append("\n CANCELLED DOWNLOADS RESTORE FAILED: \n ${it.message}")
}
//erorred downloads restore
if(json.has("errored")){
val items = json.getAsJsonArray("errored")
val errored = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
errored.add(item)
kotlin.runCatching {
if(json.has("errored")){
val items = json.getAsJsonArray("errored")
val errored = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
errored.add(item)
}
}
errored.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
errored.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(errored.isNotEmpty()){
finalMessage.append("${getString(R.string.errored)}: ${errored.count()}\n")
}
}
if(errored.isNotEmpty()){
finalMessage.append("${getString(R.string.errored)}: ${errored.count()}\n")
}
}.onFailure {
errorMessage.append("\n ERRORED DOWNLOADS RESTORE FAILED: \n ${it.message}")
}
//erorred downloads restore
if(json.has("saved")){
val items = json.getAsJsonArray("saved")
val saved = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
saved.add(item)
//saved downloads restore
kotlin.runCatching {
if(json.has("saved")){
val items = json.getAsJsonArray("saved")
val saved = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
saved.add(item)
}
}
saved.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
saved.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(saved.isNotEmpty()){
finalMessage.append("${getString(R.string.saved)}: ${saved.count()}\n")
}
}
if(saved.isNotEmpty()){
finalMessage.append("${getString(R.string.saved)}: ${saved.count()}\n")
}
}.onFailure {
errorMessage.append("\n SAVED DOWNLOADS RESTORE FAILED: \n ${it.message}")
}
//cookies restore
if(json.has("cookies")){
val items = json.getAsJsonArray("cookies")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
cookieViewModel.insert(item)
kotlin.runCatching {
if(json.has("cookies")){
val items = json.getAsJsonArray("cookies")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
cookieViewModel.insert(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.cookies)}: ${items.count()}\n")
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.cookies)}: ${items.count()}\n")
}
}.onFailure {
errorMessage.append("\n COOKIES RESTORE FAILED: \n ${it.message}")
}
//command template restore
if(json.has("templates")){
val items = json.getAsJsonArray("templates")
val templates = mutableListOf<CommandTemplate>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java)
item.id = 0L
templates.add(item)
kotlin.runCatching {
if(json.has("templates")){
val items = json.getAsJsonArray("templates")
val templates = mutableListOf<CommandTemplate>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java)
item.id = 0L
templates.add(item)
}
}
templates.asReversed().forEach { t ->
withContext(Dispatchers.IO){
commandTemplateViewModel.insert(t)
templates.asReversed().forEach { t ->
withContext(Dispatchers.IO){
commandTemplateViewModel.insert(t)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.command_templates)}: ${items.count()}\n")
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.command_templates)}: ${items.count()}\n")
}
}.onFailure {
errorMessage.append("\n COMMAND TEMPLATE RESTORE FAILED: \n ${it.message}")
}
//shortcuts restore
if(json.has("shortcuts")){
val items = json.getAsJsonArray("shortcuts")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), TemplateShortcut::class.java)
item.id = 0L
withContext(Dispatchers.IO){
commandTemplateViewModel.insertShortcut(item)
kotlin.runCatching {
if(json.has("shortcuts")){
val items = json.getAsJsonArray("shortcuts")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), TemplateShortcut::class.java)
item.id = 0L
withContext(Dispatchers.IO){
commandTemplateViewModel.insertShortcut(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.shortcuts)}: ${items.count()}\n")
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.shortcuts)}: ${items.count()}\n")
}
}.onFailure {
errorMessage.append("\n COMMAND SHORTCUTS RESTORE FAILED: \n ${it.message}")
}
//search history restore
if(json.has("search_history")){
val items = json.getAsJsonArray("search_history")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), SearchHistoryItem::class.java)
withContext(Dispatchers.IO){
resultViewModel.addSearchQueryToHistory(item.query)
kotlin.runCatching {
if(json.has("search_history")){
val items = json.getAsJsonArray("search_history")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), SearchHistoryItem::class.java)
withContext(Dispatchers.IO){
resultViewModel.addSearchQueryToHistory(item.query)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.search_history)}: ${items.count()}\n")
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.search_history)}: ${items.count()}\n")
}
}.onFailure {
errorMessage.append("\n SEARCH HISTORY RESTORE FAILED: \n ${it.message}")
}
//observe sources restore
kotlin.runCatching {
if(json.has("observe_sources")){
val items = json.getAsJsonArray("observe_sources")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), ObserveSourcesItem::class.java)
withContext(Dispatchers.IO){
item.id = 0L
observeSourcesViewModel.insert(item)
}
}
}
}.onFailure {
errorMessage.append("\n SEARCH HISTORY RESTORE FAILED: \n ${it.message}")
}
if (finalMessage.isEmpty()) throw Exception("")
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.restore))
builder.setMessage(finalMessage)
builder.setMessage("${getString(R.string.restore_complete)}\n $finalMessage \n $errorMessage")
builder.setPositiveButton(
getString(R.string.restart)
) { _: DialogInterface?, _: Int ->

View file

@ -2,7 +2,6 @@ package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import android.widget.TextView
@ -21,6 +22,7 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.updating
private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null
private var ytdlSource: Preference? = null
private var updateUtil: UpdateUtil? = null
private var version: Preference? = null
@ -34,52 +36,22 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
val editor = preferences.edit()
updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version")
ytdlSource = findPreference("ytdlp_source")
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
ytdlSource?.setOnPreferenceChangeListener { preference, newValue ->
initYTDLUpdate(editor)
true
}
updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
runCatching {
when (updateUtil!!.updateYoutubeDL()) {
YoutubeDL.UpdateStatus.DONE -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytld_update_success),
Snackbar.LENGTH_LONG).show()
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
}
YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
requireContext().getString(R.string.you_are_in_latest_version),
Snackbar.LENGTH_LONG).show()
else -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.errored),
Snackbar.LENGTH_LONG).show()
}
}
}.onFailure {
val msg = it.message ?: requireContext().getString(R.string.errored)
val snackBar = Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG)
snackBar.setAction(android.R.string.copy){
UiUtil.copyToClipboard(msg, requireActivity())
}
val snackbarView: View = snackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snackBar.show()
}
}
initYTDLUpdate(editor)
true
}
@ -119,5 +91,44 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
}
private fun initYTDLUpdate(editor: SharedPreferences.Editor) = lifecycleScope.launch {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
runCatching {
when (updateUtil!!.updateYoutubeDL()) {
YoutubeDL.UpdateStatus.DONE -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytld_update_success),
Snackbar.LENGTH_LONG).show()
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
}
YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
requireContext().getString(R.string.you_are_in_latest_version),
Snackbar.LENGTH_LONG).show()
else -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.errored),
Snackbar.LENGTH_LONG).show()
}
}
}.onFailure {
val msg = it.message ?: requireContext().getString(R.string.errored)
val snackBar = Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG)
snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, requireActivity())
}
val snackbarView: View = snackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snackBar.show()
}
}
}

View file

@ -17,9 +17,9 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdlnis.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator

View file

@ -35,11 +35,11 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.Extensions.setCustomTextSize
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.Extensions.setCustomTextSize
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -228,6 +228,8 @@ class TerminalFragment : Fragment() {
input!!.requestFocus()
input!!.setSelection(input!!.text.length)
output?.text = bundle?.getString("output") ?: output?.text
output?.isVisible = output?.text.toString().isNotEmpty()
if (bundle?.getBoolean("run") == true){
showCancelFab()
}
@ -326,6 +328,7 @@ class TerminalFragment : Fragment() {
requireActivity().runOnUiThread{
if (it != null){
if (!it.log.isNullOrBlank()) {
output?.isVisible = true
output?.text = it.log
}
output?.scrollTo(0, output!!.height)

View file

@ -1,92 +0,0 @@
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

@ -22,16 +22,13 @@ import android.view.animation.Interpolator
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.Px
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.google.android.material.tabs.TabLayout
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
@ -205,8 +202,6 @@ object Extensions {
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 ->
@ -219,6 +214,25 @@ object Extensions {
animator.start()
}
fun TabLayout.Tab.createBadge(nr: Int){
this.orCreateBadge.apply {
number = nr
verticalOffset = 5
horizontalOffset = 10
}
}
fun String.appendLineToLog(line: String): String {
val lines = this.split("\n")
//clean dublicate progress + add newline
var newLine = line
if (newLine.contains("[download")) {
newLine = "[download]" + line.split("[download]").last()
}
return lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}"
}
fun List<String>.closestValue(value: String) = minBy { abs(value.toInt() - it.toInt()) }
class CustomInterpolator : Interpolator {

View file

@ -35,6 +35,7 @@ import java.io.InputStreamReader
import java.lang.reflect.Type
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
@ -53,7 +54,7 @@ class InfoUtil(private val context: Context) {
}
fun search(query: String): ArrayList<ResultItem?> {
fun search(query: String): ArrayList<ResultItem> {
return runCatching {
when(sharedPreferences.getString("search_engine", "ytsearch")){
"ytsearch" -> searchFromPiped(query)
@ -66,8 +67,8 @@ class InfoUtil(private val context: Context) {
}
@Throws(JSONException::class)
fun searchFromPiped(query: String): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>()
fun searchFromPiped(query: String): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val data = genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCODE}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
@ -85,8 +86,8 @@ class InfoUtil(private val context: Context) {
}
@Throws(JSONException::class)
fun searchFromPipedMusic(query: String): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>()
fun searchFromPipedMusic(query: String): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCODE}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
@ -106,7 +107,7 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
try{
val items = arrayListOf<ResultItem?>()
val items = arrayListOf<ResultItem>()
// -------------- PIPED API FUNCTION -------------------
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
@ -120,10 +121,11 @@ class InfoUtil(private val context: Context) {
for (i in 0 until dataArray.length()){
kotlin.runCatching {
val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))
itm?.playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(itm)
createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))?.apply {
playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(this)
}
}
}
if (nextpage == "null") nextpage = ""
@ -137,17 +139,25 @@ class InfoUtil(private val context: Context) {
}
@Throws(JSONException::class)
fun getYoutubeVideo(url: String): List<ResultItem?> {
fun getYoutubeVideo(url: String): List<ResultItem>? {
val theURL = url.replace("\\?list.*".toRegex(), "")
return try {
try{
val id = getIDFromYoutubeURL(theURL)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(theURL) else listOf(createVideoFromPipedJSON(res, theURL))
if (res.length() == 0) {
return getFromYTDL(theURL)
}
createVideoFromPipedJSON(res, theURL)?.apply {
return listOf(this)
}
}catch (e: Exception){
val v = getFromYTDL(theURL)
v.forEach { it!!.url = theURL }
v
v.forEach { it.url = theURL }
return v
}
return null
}
private fun createVideofromJSON(obj: JSONObject): ResultItem? {
@ -359,7 +369,7 @@ class InfoUtil(private val context: Context) {
}
@Throws(JSONException::class)
fun getTrending(): ArrayList<ResultItem?> {
fun getTrending(): ArrayList<ResultItem> {
if (sharedPreferences.getString("api_key", "")!!.isNotBlank()){
return getTrendingFromYoutubeAPI()
}
@ -367,8 +377,8 @@ class InfoUtil(private val context: Context) {
}
@Throws(JSONException::class)
fun getTrendingFromYoutubeAPI(): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>()
fun getTrendingFromYoutubeAPI(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val key = sharedPreferences.getString("api_key", "")!!
val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10&regionCode=$countryCODE&maxResults=25&key=$key"
//short data
@ -397,8 +407,8 @@ class InfoUtil(private val context: Context) {
return items
}
private fun getTrendingFromPiped(): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>()
private fun getTrendingFromPiped(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val url = "$pipedURL/trending?region=$countryCODE"
val res = genericArrayRequest(url)
for (i in 0 until res.length()) {
@ -438,7 +448,7 @@ class InfoUtil(private val context: Context) {
return try {
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url)[0]!!
if (res.length() == 0) getFromYTDL(url)[0]
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
item!!.formats
}catch(e: Exception) {
@ -571,8 +581,8 @@ class InfoUtil(private val context: Context) {
}
@SuppressLint("RestrictedApi")
fun getFromYTDL(query: String): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>()
fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
val request : YoutubeDLRequest
@ -596,7 +606,7 @@ class InfoUtil(private val context: Context) {
}
request.addOption("--flat-playlist")
request.addOption("-j")
request.addOption(if (singleItem) "-J" else "-j")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--compat-options", "manifest-filesize-approx")
@ -694,14 +704,15 @@ class InfoUtil(private val context: Context) {
val url = if (jsonObject.has("url") && results.size > 1){
jsonObject.getString("url")
}else{
if (Patterns.WEB_URL.matcher(query).matches()){
if (Patterns.WEB_URL.matcher(query).matches() && playlistURL?.isEmpty() == true){
query
}else{
jsonObject.getStringByAny("webpage_url", "original_url", "url", )
jsonObject.getString("webpage_url")
}
}
items.add(ResultItem(0,
val res = ResultItem(0,
url,
title,
author,
@ -715,7 +726,8 @@ class InfoUtil(private val context: Context) {
playlistURL,
playlistIndex
)
)
items.add(res)
}
return items
}
@ -772,79 +784,6 @@ class InfoUtil(private val context: Context) {
return formats
}
fun getMissingInfo(url: String): ResultItem? {
val request = YoutubeDLRequest(url)
request.addOption("--flat-playlist")
request.addOption("-J")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val jsonObject = JSONObject(youtubeDLResponse.out)
val title = jsonObject.getStringByAny("track", "alt_title", "title", "webpage_url_basename")
val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){
duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
}
var thumb: String? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
val isPlaylist = jsonObject.has("playlist_count")
return ResultItem(
0,
url,
if (isPlaylist) "" else title,
author,
duration,
thumb!!,
jsonObject.getString("extractor"),
if (isPlaylist) jsonObject.getString("title") else "",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
}
fun getSearchSuggestions(query: String): ArrayList<String> {
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query"
@ -1064,7 +1003,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", "jpg")
request.addOption("--convert-thumbnails", sharedPreferences.getString("thumbnail_format", "jpg")!!)
}
if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime")
@ -1093,15 +1032,42 @@ class InfoUtil(private val context: Context) {
}
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "title", ".+", downloadItem.title))
try {
val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##replaceMetadata.txt")
YoutubeDLRequest("").apply {
if (downloadItem.playlistTitle.isNotBlank()){
addCommands(listOf("--replace-in-metadata", "playlist", ".+", downloadItem.playlistTitle))
runCatching {
if (downloadItem.playlistIndex != null){
addOption("--parse-metadata", downloadItem.playlistIndex.toString() + ":%(playlist_index)s")
}
}
}
if(downloadItem.title.isNotBlank()){
addCommands(listOf("--replace-in-metadata", "title", ".+", downloadItem.title))
}
if (downloadItem.author.isNotBlank()){
addCommands(listOf("--replace-in-metadata", "uploader", ".+", downloadItem.author))
}
val configData = parseYTDLRequestString(this).replace(" \" \"", "")
config.writeText(configData)
request.addOption("--config", config.absolutePath)
}
} catch (e: Exception) {
e.printStackTrace()
}
request.addOption("--parse-metadata", "%(channel,uploader,creator,artist|null)s:%(uploader)s")
request.addOption("--parse-metadata", "%(uploader)s:%(album_artist)s")
if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "uploader", ".+", downloadItem.author))
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
request.addOption("--parse-metadata", "uploader:(?P<uploader>.+)(?: - Topic)$")
if (embedMetadata){
request.addOption("--parse-metadata", "%(uploader,channel,creator,artist|null)s:%(uploader)s")
}
if (downloadItem.downloadSections.isNotBlank()){
@ -1122,7 +1088,7 @@ class InfoUtil(private val context: Context) {
"%(section_title&{} |)s$filenameTemplate"
}
if (downloadItem.downloadSections.split(";").size > 1){
filenameTemplate = "%(autonumber)d. %(section_start>%H%M%S)s $filenameTemplate"
filenameTemplate = "%(autonumber)d. $filenameTemplate [%(section_start>%H%M%S)s]"
}
}
@ -1133,7 +1099,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.extraCommands.isNotBlank()){
val cache = File(FileUtil.getCachePath(context))
cache.mkdirs()
val conf = File(cache.absolutePath + "/${System.currentTimeMillis()}.txt")
val conf = File(cache.absolutePath + "/${System.currentTimeMillis()}${UUID.randomUUID()}.txt")
conf.createNewFile()
conf.writeText(downloadItem.extraCommands)
request.addOption(
@ -1146,16 +1112,9 @@ class InfoUtil(private val context: Context) {
request.addOption("--write-description")
}
filenameTemplate = filenameTemplate.replace("%(uploader)s", "%(uploader,channel)s")
}
if (downloadItem.playlistTitle.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "playlist", ".+", downloadItem.playlistTitle))
runCatching {
if (downloadItem.playlistIndex != null){
request.addOption("--parse-metadata", downloadItem.playlistIndex.toString() + ":%(playlist_index)s")
}
}
filenameTemplate = filenameTemplate
.replace("%(uploader)s", "%(uploader,channel).30B")
.replace("%(title)s", "%(title).170B")
}
if (sharedPreferences.getBoolean("download_archive", false)){
@ -1216,9 +1175,9 @@ class InfoUtil(private val context: Context) {
if (embedMetadata){
request.addOption("--embed-metadata")
request.addOption("--parse-metadata", "artist:(?P<meta_album_artist>[^,]+)")
request.addOption("--parse-metadata", "%(album_artist,meta_album_artist,uploader)s:%(album_artist)s")
request.addOption("--parse-metadata", "%(release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s")
request.addOption("--parse-metadata", "video:%(uploader)s:%(album_artist)s")
if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
@ -1519,7 +1478,7 @@ class InfoUtil(private val context: Context) {
class PlaylistTuple internal constructor(
var nextPageToken: String,
var videos: ArrayList<ResultItem?>
var videos: ArrayList<ResultItem>
)
companion object {

View file

@ -7,19 +7,15 @@ import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver
@ -27,7 +23,6 @@ import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.ResumeActivity
import com.deniscerri.ytdlnis.receiver.ShareFileActivity
import java.io.File
import java.util.Locale
class NotificationUtil(var context: Context) {

View file

@ -6,13 +6,11 @@ import android.content.pm.PackageManager
import android.text.Spanned
import android.util.TypedValue
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R
import com.google.android.material.color.DynamicColors

View file

@ -2,7 +2,6 @@ package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -10,6 +9,8 @@ import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat
@ -25,20 +26,16 @@ import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.ui.text.toUpperCase
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.FileProvider
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.core.view.setMargins
import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager
@ -61,7 +58,6 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.AlreadyExistsAdapter
import com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
import com.deniscerri.ytdlnis.util.Extensions.dp
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration
@ -94,7 +90,6 @@ import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@ -496,6 +491,7 @@ object UiUtil {
fun showDatePicker(fragmentManager: FragmentManager , onSubmit : (chosenDate: Calendar) -> Unit ){
val currentDate = Calendar.getInstance()
currentDate.timeInMillis = (currentDate.timeInMillis - (currentDate.timeInMillis % 1800000)) + 1800000
val date = Calendar.getInstance()
val datePicker = MaterialDatePicker.Builder.datePicker()
@ -530,6 +526,7 @@ object UiUtil {
fun showTimePicker(fragmentManager: FragmentManager , onSubmit : (chosenTime: Calendar) -> Unit ){
val currentDate = Calendar.getInstance()
currentDate.timeInMillis = (currentDate.timeInMillis - (currentDate.timeInMillis % 1800000)) + 1800000
val date = Calendar.getInstance()
val timepicker = MaterialTimePicker.Builder()
@ -1055,16 +1052,22 @@ object UiUtil {
val chips = mutableListOf<Chip>()
context.getStringArray(R.array.subtitle_langs).forEachIndexed { index, s ->
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
tmp.text = s
tmp.text = Locale(s).displayLanguage
tmp.tag = s
tmp.id = index
tmp.setOnClickListener {
val c = it as Chip
if(!c.isChecked){
editText.setText(editText.text.toString().replace(c.text.toString(), ""))
editText.setText(editText.text.toString().replace(c.tag.toString(), "").removeSuffix(","))
editText.setSelection(editText.text.length)
}else{
editText.append(c.text)
if (editText.text.isBlank()){
editText.setText(c.tag.toString())
editText.setSelection(editText.text.length)
}else{
editText.append(",${c.tag}")
}
}
}
@ -1183,13 +1186,14 @@ object UiUtil {
saveThumbnailClicked: (Boolean) -> Unit,
sponsorBlockItemsSet: (values: Array<String>, checkedItems: List<Boolean>) -> Unit,
cutClicked: (VideoCutListener) -> Unit,
cutDisabledClicked: () -> Unit,
filenameTemplateSet: (String) -> Unit,
saveSubtitlesClicked: (Boolean) -> Unit,
saveAutoSubtitlesClicked: (Boolean) -> Unit,
subtitleLanguagesSet: (String) -> Unit,
removeAudioClicked: (Boolean) -> Unit,
extraCommandsClicked: () -> Unit,
updateDataClicked: () -> Unit
alsoDownloadAsAudioClicked: (Boolean) -> Unit,
extraCommandsClicked: () -> Unit
){
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -1322,11 +1326,7 @@ object UiUtil {
}else{
cut.alpha = 0.3f
cut.setOnClickListener {
val snack = Snackbar.make(view, context.getString(R.string.cut_unavailable), Snackbar.LENGTH_SHORT)
snack.setAction(R.string.update){
updateDataClicked()
}
snack.show()
cutDisabledClicked()
}
}
}
@ -1372,10 +1372,41 @@ object UiUtil {
}
}
val removeAudio = view.findViewById<Chip>(R.id.remove_audio)
removeAudio.isChecked = items.all { it.videoPreferences.removeAudio }
removeAudio.setOnCheckedChangeListener { _, _ ->
removeAudioClicked(removeAudio.isChecked)
if (items.size == 1 && items.first().id == 0L){
val adjustAudio = view.findViewById<Chip>(R.id.adjust_audio)
adjustAudio.setOnClickListener {
val adjustAudioView = context.layoutInflater.inflate(R.layout.audio_download_preferences_dialog, null)
adjustAudioView.findViewById<MaterialSwitch>(R.id.remove_audio).apply {
isChecked = items.first().videoPreferences.removeAudio
setOnCheckedChangeListener { _, b ->
removeAudioClicked(b)
}
}
adjustAudioView.findViewById<MaterialSwitch>(R.id.also_download_audio).apply {
isChecked = items.first().videoPreferences.alsoDownloadAsAudio
setOnCheckedChangeListener { _, b ->
alsoDownloadAsAudioClicked(b)
}
}
val adjustAudioDialog = MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.adjust_audio))
.setView(adjustAudioView)
.setIcon(R.drawable.ic_music)
.setNegativeButton(context.resources.getString(R.string.dismiss)) { _: DialogInterface?, _: Int -> }
adjustAudioDialog.show()
}
}else{
val adjustAudio = view.findViewById<Chip>(R.id.adjust_audio)
adjustAudio.isVisible = false
val removeAudio = view.findViewById<Chip>(R.id.remove_audio)
removeAudio.isVisible = true
removeAudio.isChecked = items.all { it.videoPreferences.removeAudio }
removeAudio.setOnCheckedChangeListener { _, _ ->
removeAudioClicked(removeAudio.isChecked)
}
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
@ -1399,8 +1430,8 @@ object UiUtil {
filenameTemplateSet: (String) -> Unit,
sponsorBlockItemsSet: (Array<String>, List<Boolean>) -> Unit,
cutClicked: (VideoCutListener) -> Unit,
extraCommandsClicked: () -> Unit,
updateDataClicked: () -> Unit
cutDisabledClicked: () -> Unit,
extraCommandsClicked: () -> Unit
){
val embedThumb = view.findViewById<Chip>(R.id.embed_thumb)
val cropThumb = view.findViewById<Chip>(R.id.crop_thumb)
@ -1513,11 +1544,7 @@ object UiUtil {
}else{
cut.alpha = 0.3f
cut.setOnClickListener {
val snack = Snackbar.make(view, context.getString(R.string.cut_unavailable), Snackbar.LENGTH_SHORT)
snack.setAction(R.string.update){
updateDataClicked()
}
snack.show()
cutDisabledClicked()
}
}
}

View file

@ -26,6 +26,7 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.GithubRelease
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
@ -175,7 +176,7 @@ class UpdateUtil(var context: Context) {
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
)
layoutParams.setMargins(20, 20, 20, 0)
layoutParams.setMargins(5, 5, 5, 0)
scrollView.layoutParams = layoutParams
scrollView.addView(linearLayout)
val releases = getGithubReleases()
@ -212,7 +213,9 @@ class UpdateUtil(var context: Context) {
val assetGroup = findViewById<ChipGroup>(R.id.assets)
it.assets.forEachIndexed { idx, c ->
val tmp = activity.layoutInflater.inflate(R.layout.suggestion_chip, assetGroup, false) as Chip
val tmp = activity.layoutInflater.inflate(R.layout.filter_chip, assetGroup, false) as Chip
tmp.isCheckable = false
tmp.layoutParams = layoutParams
tmp.text = c.name
tmp.id = idx
tmp.setOnClickListener {

View file

@ -18,16 +18,15 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.workDataOf
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.deniscerri.ytdlnis.util.FileUtil
@ -36,6 +35,7 @@ import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -59,6 +59,7 @@ class DownloadWorker(
val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao)
val resultRepo = ResultRepository(dbManager.resultDao, context)
val handler = Handler(Looper.getMainLooper())
val alarmScheduler = AlarmScheduler(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -67,6 +68,8 @@ class DownloadWorker(
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success()
val createResultItem = inputData.getBoolean("createResultItem", true)
val confTmp = Configuration(context.resources.configuration)
val currLang = sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }.split("-")
confTmp.setLocale(if (currLang.size == 1) Locale(currLang[0]) else Locale(currLang[0], currLang[1]))
@ -111,8 +114,11 @@ class DownloadWorker(
val request = infoUtil.buildYoutubeDLRequest(downloadItem)
downloadItem.status = DownloadRepository.Status.Active.toString()
CoroutineScope(Dispatchers.IO).launch {
delay(1500)
//update item if its incomplete
updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
}
}
val cacheDir = FileUtil.getCachePath(context)
@ -164,7 +170,10 @@ class DownloadWorker(
}
}
}.onSuccess {
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
}
val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking {
var finalPaths : List<String>?
@ -178,12 +187,6 @@ class DownloadWorker(
.map { it.removeSuffix("'") }
.sortedBy { File(it).lastModified() }
.toList()
// finalPaths = File(FileUtil.formatPath(downloadLocation))
// .walkTopDown()
// .filter { it.isFile && p.any { f -> f.contains(it.nameWithoutExtension) }}
// .sortedByDescending { it.length() }
// .map { it.absolutePath }
// .toList()
FileUtil.scanMedia(finalPaths, context)
if (finalPaths.isEmpty()){
finalPaths = listOf(context.getString(R.string.unfound_file))
@ -212,7 +215,14 @@ class DownloadWorker(
}
}
finalPaths = finalPaths?.filter { !it.matches("(\\.description)|(\\.srt)|(\\.ass)|(\\.lrc)|(\\.vtt)|(\\.txt)|(\\.jpg)|(\\.png)\$".toRegex()) }
val nonMediaExtensions = mutableListOf<String>().apply {
addAll(context.getStringArray(R.array.thumbnail_containers_values))
addAll(context.getStringArray(R.array.sub_formats_values).filter { it.isNotBlank() })
add("description")
add("txt")
}
finalPaths = finalPaths?.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }
FileUtil.deleteConfigFiles(request)
//put download in history
@ -260,7 +270,7 @@ class DownloadWorker(
downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources
)
if (wasQuickDownloaded){
if (wasQuickDownloaded && createResultItem){
runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id))
runBlocking {
@ -338,32 +348,6 @@ class DownloadWorker(
return Result.success()
}
private fun updateDownloadItem(
downloadItem: DownloadItem,
infoUtil: InfoUtil,
dao: DownloadDao,
resultDao: ResultDao
) : Boolean {
var wasQuickDownloaded = false
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
runCatching {
if (isStopped) YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
setProgressAsync(workDataOf("progress" to 0, "output" to context.getString(R.string.updating_download_data), "id" to downloadItem.id))
val info = infoUtil.getMissingInfo(downloadItem.url)
if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString()
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
dao.update(downloadItem)
}
}
}
return wasQuickDownloaded
}
companion object {

View file

@ -6,27 +6,23 @@ import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.Data
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.database.repository.ResultRepository
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
@ -41,6 +37,7 @@ class ObserveSourceWorker(
if (sourceID == 0L) return Result.failure()
val dbManager = DBManager.getInstance(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao)
val resultsRepo = ResultRepository(dbManager.resultDao, App.instance)
val historyRepo = HistoryRepository(dbManager.historyDao)
val downloadRepo = DownloadRepository(dbManager.downloadDao)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -48,7 +45,7 @@ class ObserveSourceWorker(
val item = repo.getByID(sourceID)
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){
DownloadUtil.cancelObservationTaskByID(context, item.id)
repo.cancelObservationTaskByID(context, item.id)
return Result.failure()
}
@ -57,36 +54,50 @@ class ObserveSourceWorker(
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)
val res = runCatching { infoUtil.getFromYTDL(item.url).toList() }.getOrElse { listOf() }
.filter { result ->
//if first run and get new items only is preferred then dont get anything on first run
if (item.getOnlyNewUploads && item.runCount == 0){
item.alreadyProcessedLinks.add(result.url)
false
}else{
true
}
}
}else{
//all items that are not already processed
res.filter { !item.alreadyProcessedLinks.contains(it!!.url) }
}
.filter { result ->
val history = historyRepo.getAllByURL(result.url)
if (!item.retryMissingDownloads){
//all items that are not present in history
history.none { hi -> hi.downloadPath.any { path -> FileUtil.exists(path) } }
}else{
//all items that are not already processed
if(item.alreadyProcessedLinks.isEmpty()){
!history.map { it.url }.contains(result.url) && !item.alreadyProcessedLinks.contains(result.url)
}else{
!item.alreadyProcessedLinks.contains(result.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.url = it.url
downloadItem.title = it.title
downloadItem.author = it.author
downloadItem.thumb = it.thumb
downloadItem.status = DownloadRepository.Status.Queued.toString()
downloadItem.playlistTitle = it.playlistTitle
downloadItem.playlistURL = it.playlistURL
downloadItem.playlistIndex = it.playlistIndex
downloadItem.id = 0L
items.add(downloadItem)
}
withContext(Dispatchers.IO){
repo.update(item)
}
if (items.isNotEmpty()){
@ -108,69 +119,73 @@ class ObserveSourceWorker(
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()
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) } }
}
if (existingDownload != null) {
it.id = existingDownload.id
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{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepo.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
if (it.id == 0L){
it.id = downloadRepo.insert(it)
}else if (it.status == DownloadRepository.Status.Queued.toString()){
downloadRepo.update(it)
}
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)
}
queuedItems.add(it)
}
}
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
DownloadUtil.startDownloadWorker(queuedItems, context)
downloadRepo.startDownloadWorker(queuedItems, context, Data.Builder().putBoolean("createResultItem", false))
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){}
runCatching {
resultsRepo.updateDownloadItem(it)?.apply {
downloadRepo.updateWithoutUpsert(this)
}
}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{
updateDownloadItem(it, infoUtil, downloadRepo, dbManager.resultDao)
}catch (ignored: Exception){}
runCatching {
resultsRepo.updateDownloadItem(it)?.apply {
downloadRepo.updateWithoutUpsert(this)
}
}
}
}
}
item.alreadyProcessedLinks.removeAll(items.map { it.url })
item.alreadyProcessedLinks.addAll(items.map { it.url })
}
item.runCount = item.runCount + 1
if (item.runCount > item.endsAfterCount && item.endsAfterCount > 0){
item.status = ObserveSourcesRepository.SourceStatus.STOPPED
withContext(Dispatchers.IO){
@ -185,10 +200,14 @@ class ObserveSourceWorker(
withContext(Dispatchers.IO){
repo.update(item)
}
DownloadUtil.cancelObservationTaskByID(context, item.id)
repo.cancelObservationTaskByID(context, item.id)
return Result.success()
}
withContext(Dispatchers.IO){
repo.update(item)
}
scheduleForNextTime(item)
return Result.success()
}
@ -232,28 +251,4 @@ class ObserveSourceWorker(
)
}
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

@ -20,7 +20,6 @@ import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
@ -93,7 +92,7 @@ class TerminalDownloadWorker(
0,
"Terminal Task",
"Terminal Task\n" +
"Command: ${command}\n\n",
"Command: \n ${command}\n\n",
Format(),
DownloadViewModel.Type.command,
System.currentTimeMillis(),

View file

@ -2,9 +2,7 @@ package com.deniscerri.ytdlnis.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil

View file

@ -1,5 +1,10 @@
<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.5,10c0.17,0 0.33,0.03 0.5,0.05L20,1L1,20h13v-3c0,-0.89 0.39,-1.68 1,-2.23v-0.27c0,-2.48 2.02,-4.5 4.5,-4.5zM22,16v-1.5c0,-1.38 -1.12,-2.5 -2.5,-2.5S17,13.12 17,14.5L17,16c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1zM21,16h-3v-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5L21,16z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,600Q120,567 143.5,543.5Q167,520 200,520L600,520L600,360L680,360L680,520L760,520Q793,520 816.5,543.5Q840,567 840,600L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,600Q760,600 760,600Q760,600 760,600L200,600Q200,600 200,600Q200,600 200,600L200,760Q200,760 200,760Q200,760 200,760ZM280,720Q297,720 308.5,708.5Q320,697 320,680Q320,663 308.5,651.5Q297,640 280,640Q263,640 251.5,651.5Q240,663 240,680Q240,697 251.5,708.5Q263,720 280,720ZM420,720Q437,720 448.5,708.5Q460,697 460,680Q460,663 448.5,651.5Q437,640 420,640Q403,640 391.5,651.5Q380,663 380,680Q380,697 391.5,708.5Q403,720 420,720ZM560,720Q577,720 588.5,708.5Q600,697 600,680Q600,663 588.5,651.5Q577,640 560,640Q543,640 531.5,651.5Q520,663 520,680Q520,697 531.5,708.5Q543,720 560,720ZM570,330L512,272Q538,248 570,234Q602,220 640,220Q678,220 710,234Q742,248 768,272L710,330Q696,316 678.5,308Q661,300 640,300Q619,300 601.5,308Q584,316 570,330ZM470,230L414,174Q458,130 516,105Q574,80 640,80Q706,80 764,105Q822,130 866,174L810,230Q777,197 733.5,178.5Q690,160 640,160Q590,160 546.5,178.5Q503,197 470,230ZM200,760Q200,760 200,760Q200,760 200,760L200,600Q200,600 200,600Q200,600 200,600L200,600Q200,600 200,600Q200,600 200,600L200,760Q200,760 200,760Q200,760 200,760L200,760Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M20.5,10L21,8h-4l1,-4h-2l-1,4h-4l1,-4h-2L9,8H5l-0.5,2h4l-1,4h-4L3,16h4l-1,4h2l1,-4h4l-1,4h2l1,-4h4l0.5,-2h-4l1,-4H20.5zM13.5,14h-4l1,-4h4L13.5,14z"/>
</vector>

View file

@ -1,5 +1,11 @@
<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="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.89,2 1.99,2H15v-8h5V8L14,2zM13,9V3.5L18.5,9H13zM17,21.66V16h5.66v2h-2.24l2.95,2.95l-1.41,1.41L19,19.41l0,2.24H17z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M280,680L560,680L560,600L280,600L280,680ZM280,520L680,520L680,440L280,440L280,520ZM280,360L680,360L680,280L280,280L280,360ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
</vector>

View file

@ -1,6 +1,10 @@
<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="M18,6.41l-1.41,-1.41l-4.59,4.58l-4.59,-4.58l-1.41,1.41l6,6z"/>
<path android:fillColor="?android:colorAccent" android:pathData="M18,13l-1.41,-1.41l-4.59,4.58l-4.59,-4.58l-1.41,1.41l6,6z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M120,800L120,720L840,720L840,800L120,800ZM120,240L120,160L840,160L840,240L120,240ZM200,640Q167,640 143.5,616.5Q120,593 120,560L120,400Q120,367 143.5,343.5Q167,320 200,320L760,320Q793,320 816.5,343.5Q840,367 840,400L840,560Q840,593 816.5,616.5Q793,640 760,640L200,640Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M300,240Q275,240 257.5,257.5Q240,275 240,300Q240,325 257.5,342.5Q275,360 300,360Q325,360 342.5,342.5Q360,325 360,300Q360,275 342.5,257.5Q325,240 300,240ZM300,640Q275,640 257.5,657.5Q240,675 240,700Q240,725 257.5,742.5Q275,760 300,760Q325,760 342.5,742.5Q360,725 360,700Q360,675 342.5,657.5Q325,640 300,640ZM160,120L800,120Q817,120 828.5,131.5Q840,143 840,160L840,440Q840,457 828.5,468.5Q817,480 800,480L160,480Q143,480 131.5,468.5Q120,457 120,440L120,160Q120,143 131.5,131.5Q143,120 160,120ZM160,520L800,520Q817,520 828.5,531.5Q840,543 840,560L840,840Q840,857 828.5,868.5Q817,880 800,880L160,880Q143,880 131.5,868.5Q120,857 120,840L120,560Q120,543 131.5,531.5Q143,520 160,520Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M440,700L440,520L80,520L80,440L440,440L440,260L880,260L880,700L440,700Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q149,840 127.5,794.5Q106,749 138,710L360,440L360,200L320,200Q303,200 291.5,188.5Q280,177 280,160Q280,143 291.5,131.5Q303,120 320,120L640,120Q657,120 668.5,131.5Q680,143 680,160Q680,177 668.5,188.5Q657,200 640,200L600,200L600,440L822,710Q854,749 832.5,794.5Q811,840 760,840L200,840ZM280,720L680,720L544,560L416,560L280,720Z"/>
</vector>

View file

@ -1,5 +1,10 @@
<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.34,2.02C6.59,1.82 2,6.42 2,12c0,5.52 4.48,10 10,10c3.71,0 6.93,-2.02 8.66,-5.02C13.15,16.73 8.57,8.55 12.34,2.02z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M312,720Q261,720 214.5,702Q168,684 131,649Q83,604 61.5,542.5Q40,481 40,415Q40,337 78,288.5Q116,240 189,240Q203,240 215.5,242.5Q228,245 241,250L480,339L719,250Q732,245 744.5,242.5Q757,240 771,240Q844,240 882,288.5Q920,337 920,415Q920,481 898.5,542.5Q877,604 829,649Q792,684 745.5,702Q699,720 648,720Q582,720 536,690Q490,660 490,660L470,660Q470,660 424,690Q378,720 312,720ZM349,560Q378,560 395,546.5Q412,533 412,510Q412,471 360,435.5Q308,400 251,400Q222,400 205,413.5Q188,427 188,450Q188,489 240,524.5Q292,560 349,560ZM611,560Q668,560 720,524.5Q772,489 772,450Q772,426 755.5,413Q739,400 709,400Q652,400 600,435.5Q548,471 548,510Q548,533 564.5,546.5Q581,560 611,560Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M600,600L680,600L680,200L600,200L600,360L520,360L520,200L440,200L440,440L600,440L600,600ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739L480,739Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="M84,444L0,360Q92,266 215,213Q338,160 480,160Q504,160 528,161.5Q552,163 576,166L516,282Q504,280 496.5,280Q489,280 480,280Q364,280 262.5,323.5Q161,367 84,444ZM254,614L170,528Q226,472 298.5,439.5Q371,407 456,402L392,532Q353,544 318,564.5Q283,585 254,614ZM452,794Q422,783 405.5,752.5Q389,722 404,692L644,204Q648,196 655.5,193.5Q663,191 672,194Q680,197 684,204Q688,211 686,220L556,746Q548,779 516,792Q484,805 452,794ZM706,614Q700,608 693,602Q686,596 678,590L710,464Q731,479 751.5,494.5Q772,510 790,528L706,614ZM876,444Q846,414 811.5,389Q777,364 738,344L766,224Q820,250 869,284Q918,318 960,360L876,444Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="19dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
</vector>

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