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 properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 2 def versionPatch = 3
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = "" 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" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <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.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.Gravity
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach import androidx.core.view.forEach

View file

@ -1,14 +1,30 @@
package com.deniscerri.ytdlnis.database package com.deniscerri.ytdlnis.database
import android.content.Context import android.content.Context
import androidx.room.* import androidx.room.AutoMigration
import androidx.room.migration.AutoMigrationSpec import androidx.room.Database
import androidx.room.migration.Migration import androidx.room.Room
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.room.RoomDatabase
import androidx.work.impl.Migration_11_12 import androidx.room.TypeConverters
import androidx.work.impl.Migration_12_13 import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.dao.* import com.deniscerri.ytdlnis.database.dao.CookieDao
import com.deniscerri.ytdlnis.database.models.* 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) @TypeConverters(Converters::class)
@Database( @Database(
@ -24,7 +40,7 @@ import com.deniscerri.ytdlnis.database.models.*
TerminalItem::class, TerminalItem::class,
ObserveSourcesItem::class ObserveSourcesItem::class
], ],
version = 14, version = 15,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
@ -39,6 +55,7 @@ import com.deniscerri.ytdlnis.database.models.*
AutoMigration (from = 11, to = 12), AutoMigration (from = 11, to = 12),
AutoMigration (from = 12, to = 13), AutoMigration (from = 12, to = 13),
// AutoMigration (from = 13, to = 14) MANUALLY HANDLED // AutoMigration (from = 13, to = 14) MANUALLY HANDLED
AutoMigration (from = 14, to = 15),
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){

View file

@ -1,6 +1,10 @@
package com.deniscerri.ytdlnis.database.dao 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.CommandTemplate
import com.deniscerri.ytdlnis.database.models.TemplateShortcut import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View file

@ -1,6 +1,10 @@
package com.deniscerri.ytdlnis.database.dao 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 com.deniscerri.ytdlnis.database.models.CookieItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -10,7 +14,7 @@ interface CookieDao {
fun getAllCookies() : List<CookieItem> fun getAllCookies() : List<CookieItem>
@Query("SELECT * FROM cookies WHERE url = :url LIMIT 1") @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") @Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookiesFlow() : Flow<List<CookieItem>> fun getAllCookiesFlow() : Flow<List<CookieItem>>

View file

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

View file

@ -1,6 +1,11 @@
package com.deniscerri.ytdlnis.database.dao 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 com.deniscerri.ytdlnis.database.models.LogItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View file

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

View file

@ -1,6 +1,11 @@
package com.deniscerri.ytdlnis.database.dao 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 com.deniscerri.ytdlnis.database.models.ResultItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -27,6 +32,11 @@ interface ResultDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMultiple(items: List<ResultItem?>) : List<Long> 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) @Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ResultItem) suspend fun update(item: ResultItem)

View file

@ -1,6 +1,9 @@
package com.deniscerri.ytdlnis.database.dao 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 import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
@Dao @Dao

View file

@ -1,7 +1,11 @@
package com.deniscerri.ytdlnis.database.dao 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.database.models.TerminalItem
import com.deniscerri.ytdlnis.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@ -28,16 +32,7 @@ interface TerminalDao {
fun updateLog(line: String, id: Long){ fun updateLog(line: String, id: Long){
val t = getTerminalById(id) ?: return val t = getTerminalById(id) ?: return
val log = t.log ?: "" val log = t.log ?: ""
val lines = log.split("\n") updateTerminalLog(log.appendLineToLog(line), id)
//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)
} }
@Insert @Insert

View file

@ -7,8 +7,6 @@ import androidx.room.PrimaryKey
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Month import java.time.Month
import java.util.Calendar
import java.util.Date
@Entity(tableName = "observeSources") @Entity(tableName = "observeSources")
@Parcelize @Parcelize
@ -32,6 +30,8 @@ data class ObserveSourcesItem(
var endsAfterCount: Int, var endsAfterCount: Int,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
var runCount: Int, var runCount: Int,
@ColumnInfo(defaultValue = "0")
var getOnlyNewUploads: Boolean,
var retryMissingDownloads: Boolean, var retryMissingDownloads: Boolean,
var alreadyProcessedLinks : MutableList<String> var alreadyProcessedLinks : MutableList<String>
) : Parcelable ) : Parcelable

View file

@ -1,10 +1,5 @@
package com.deniscerri.ytdlnis.database.models 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( data class SearchSuggestionItem(
var text: String, var text: String,
val type: SearchSuggestionType, val type: SearchSuggestionType,

View file

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

View file

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

View file

@ -1,15 +1,32 @@
package com.deniscerri.ytdlnis.database.repository 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.Pager
import androidx.paging.PagingConfig 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.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.work.DownloadWorker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
class DownloadRepository(private val downloadDao: DownloadDao) { class DownloadRepository(private val downloadDao: DownloadDao) {
@ -36,12 +53,20 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getSavedDownloads()} 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 { enum class Status {
Active, ActivePaused, PausedReQueued, Queued, QueuedPaused, Error, Cancelled, Saved, Processing 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 { suspend fun insert(item: DownloadItem) : Long {
return downloadDao.insert(item) return downloadDao.insert(item)
} }
@ -162,4 +187,53 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.removeAllLogID() 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 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.dao.HistoryDao
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import com.deniscerri.ytdlnis.database.DBManager.SORTING
class HistoryRepository(private val historyDao: HistoryDao) { class HistoryRepository(private val historyDao: HistoryDao) {
val items : Flow<List<HistoryItem>> = historyDao.getAllHistory() 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.dao.LogDao
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.regex.MatchResult
import java.util.regex.Pattern
class LogRepository(private val logDao: LogDao) { class LogRepository(private val logDao: LogDao) {
val items : Flow<List<LogItem>> = logDao.getAllLogsFlow() val items : Flow<List<LogItem>> = logDao.getAllLogsFlow()
@ -43,16 +42,7 @@ class LogRepository(private val logDao: LogDao) {
runCatching { runCatching {
val item = getItem(id) ?: return val item = getItem(id) ?: return
val log = item.content ?: "" val log = item.content ?: ""
val lines = log.split("\n") item.content = log.appendLineToLog(line)
//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 += "\n$line" //item.content += "\n$line"
logDao.update(item) logDao.update(item)
} }

View file

@ -1,8 +1,13 @@
package com.deniscerri.ytdlnis.database.repository 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.R
import com.deniscerri.ytdlnis.database.dao.ObserveSourcesDao import com.deniscerri.ytdlnis.database.dao.ObserveSourcesDao
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao) { class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao) {
@ -57,4 +62,16 @@ class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao)
observeSourcesDao.update(item) 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 package com.deniscerri.ytdlnis.database.repository
import android.content.Context import android.content.Context
import android.util.Patterns
import com.deniscerri.ytdlnis.database.dao.ResultDao 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.database.models.ResultItem
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern
class ResultRepository(private val resultDao: ResultDao, private val context: Context) { class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
private val tag: String = "ResultRepository" private val tag: String = "ResultRepository"
val allResults : Flow<List<ResultItem>> = resultDao.getResults() val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1) var itemCount = MutableStateFlow(-1)
enum class SourceType {
YOUTUBE_VIDEO, YOUTUBE_PLAYLIST, SEARCH_QUERY, YT_DLP
}
suspend fun insert(it: ResultItem){ suspend fun insert(it: ResultItem){
resultDao.insert(it) resultDao.insert(it)
} }
@ -28,46 +35,52 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
resultDao.insertMultiple(items) 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) val infoUtil = InfoUtil(context)
if (resetResults) deleteAll() if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery) val res = infoUtil.search(inputQuery)
itemCount.value = res.size itemCount.value = res.size
val ids = resultDao.insertMultiple(res) if (addToResults){
ids.forEachIndexed { index, id -> val ids = resultDao.insertMultiple(res)
res[index]?.id = id ids.forEachIndexed { index, id ->
res[index].id = id
}
} }
return res 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 infoUtil = InfoUtil(context)
val v = infoUtil.getYoutubeVideo(inputQuery) val v = infoUtil.getYoutubeVideo(inputQuery) ?: return arrayListOf()
if (resetResults) { if (resetResults) {
deleteAll() deleteAll()
itemCount.value = v.size itemCount.value = v.size
}else{ }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) if (addToResults){
ids.forEachIndexed { index, id -> val ids = resultDao.insertMultiple(v)
v[index]?.id = id ids.forEachIndexed { index, id ->
v[index].id = id
}
} }
return ArrayList(v) 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() } val query = inputQuery.split("list=".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1].split("&").first() .toTypedArray()[1].split("&").first()
var nextPageToken = "" var nextPageToken = ""
if (resetResults) deleteAll() if (resetResults) deleteAll()
val infoUtil = InfoUtil(context) val infoUtil = InfoUtil(context)
val items : ArrayList<ResultItem?> = arrayListOf() val items = arrayListOf<ResultItem>()
do { do {
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0]?.playlistTitle ?: "" else "") val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0].playlistTitle else "")
val ids = resultDao.insertMultiple(tmp.videos.toList()) if (addToResults){
ids.forEachIndexed { index, id -> val ids = resultDao.insertMultiple(tmp.videos.toList())
tmp.videos[index]?.id = id ids.forEachIndexed { index, id ->
tmp.videos[index].id = id
}
} }
items.addAll(tmp.videos) items.addAll(tmp.videos)
val tmpToken = tmp.nextPageToken val tmpToken = tmp.nextPageToken
@ -79,19 +92,23 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return items 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 infoUtil = InfoUtil(context)
val items = infoUtil.getFromYTDL(inputQuery) val items = infoUtil.getFromYTDL(inputQuery, singleItem)
if (resetResults) { if (resetResults) {
deleteAll() deleteAll()
itemCount.value = items.size itemCount.value = items.size
}else{ }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 -> if (addToResults){
items[index]?.id = id val ids = resultDao.insertMultiple(items.toList())
ids.forEachIndexed { index, id ->
items[index].id = id
}
} }
return items return items
} }
@ -111,4 +128,59 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
fun getItemByURL(url: String): ResultItem? { fun getItemByURL(url: String): ResultItem? {
return resultDao.getResultByURL(url) 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.DBManager
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.database.repository.CookieRepository
import com.deniscerri.ytdlnis.ui.more.CookiesFragment
import com.deniscerri.ytdlnis.ui.more.WebViewActivity import com.deniscerri.ytdlnis.ui.more.WebViewActivity
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.net.URL
import java.util.Date import java.util.Date
class CookieViewModel(private val application: Application) : AndroidViewModel(application) { class CookieViewModel(private val application: Application) : AndroidViewModel(application) {
@ -44,7 +43,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
return repository.getAll() return repository.getAll()
} }
fun getByURL(url: String) : CookieItem { fun getByURL(url: String) : CookieItem? {
return repository.getByURL(url) return repository.getByURL(url)
} }
@ -75,7 +74,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
} }
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
fun getCookiesFromDB() : Result<String> = kotlin.runCatching { fun getCookiesFromDB(url: String) : Result<String> = kotlin.runCatching {
CookieManager.getInstance().run { CookieManager.getInstance().run {
if (!hasCookies()) throw Exception("There is no cookies in the database!") if (!hasCookies()) throw Exception("There is no cookies in the database!")
flush() 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" } val dbPath = File("/data/data/${BuildConfig.APPLICATION_ID}/").walkTopDown().find { it.name == "Cookies" }
?: throw Exception("Cookies File not found!") ?: throw Exception("Cookies File not found!")
SQLiteDatabase.openDatabase( val db = SQLiteDatabase.openDatabase(
dbPath.absolutePath, null, OPEN_READONLY 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( cookieList.add(
WebViewActivity.CookieItem( WebViewActivity.CookieItem(
domain = host, domain = host,
@ -118,13 +122,13 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
) )
) )
} }
close()
} }
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) { fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) {

View file

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

View file

@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.AlarmManager import android.app.AlarmManager
import android.app.Application import android.app.Application
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
@ -12,12 +11,9 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.DBManager 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.models.ObserveSourcesItem
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Month import java.time.Month
@ -63,13 +59,13 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
} }
fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) { fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) {
runCatching { DownloadUtil.cancelObservationTaskByID(application, item.id) } runCatching { repository.cancelObservationTaskByID(application, item.id) }
repository.delete(item) repository.delete(item)
} }
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
getAll().forEach { getAll().forEach {
runCatching { DownloadUtil.cancelObservationTaskByID(application, it.id) } runCatching { repository.cancelObservationTaskByID(application, it.id) }
} }
repository.deleteAll() repository.deleteAll()
@ -94,7 +90,7 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
c.set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE)) c.set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
DownloadUtil.cancelObservationTaskByID(application, id) repository.cancelObservationTaskByID(application, id)
val intent = Intent(application, ObserveAlarmReceiver::class.java) val intent = Intent(application, ObserveAlarmReceiver::class.java)
intent.putExtra("id", id) intent.putExtra("id", id)

View file

@ -5,7 +5,6 @@ import android.app.ActivityManager.RunningAppProcessInfo
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import android.util.Patterns
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData 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.database.repository.SearchHistoryRepository
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import java.util.concurrent.CancellationException
import java.util.regex.Pattern
class ResultViewModel(private val application: Application) : AndroidViewModel(application) { class ResultViewModel(private val application: Application) : AndroidViewModel(application) {
@ -53,6 +52,18 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
errorMessage = null, errorMessage = null,
actions = 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 private val sharedPreferences: SharedPreferences
init { init {
@ -86,41 +97,45 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
deleteAll() 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)} fun cancelParsingQueries(){
return withContext(Dispatchers.IO){ parsingQueries?.cancel(CancellationException())
val res = mutableListOf<ResultItem?>() uiState.update {it.copy(processing = false)}
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())
}
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){ }else{
notificationUtil.showQueriesFinished() onResult(listOf())
}
uiState.update {it.copy(processing = false)}
res
} }
} }
@ -133,20 +148,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
}.getOrElse { true } }.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){ fun insert(items: ArrayList<ResultItem?>) = viewModelScope.launch(Dispatchers.IO){
items.forEach { 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){ suspend fun updateItemData(res: ResultItem){
if (updateResultDataJob == null || updateResultDataJob?.isCancelled == true || updateResultDataJob?.isCompleted == true){ if (updateResultDataJob == null || updateResultDataJob?.isCancelled == true || updateResultDataJob?.isCompleted == true){
updateResultDataJob = viewModelScope.launch(Dispatchers.IO) { updateResultDataJob = viewModelScope.launch(Dispatchers.IO) {
updatingData.emit(true) updatingData.emit(true)
val result = parseQueries(listOf(res.url)) updatingData.value = true
updatingData.emit(false) parseQueries(listOf(res.url)){ result ->
updateResultData.emit(result) viewModelScope.launch(Dispatchers.IO){
updatingData.emit(false)
updatingData.value = false
updateResultData.emit(result)
}
}
} }
updateResultDataJob?.start() updateResultDataJob?.start()
@ -218,6 +218,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
if (it != null){ if (it != null){
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
updatingData.emit(false) updatingData.emit(false)
updatingData.value = false
updateResultData.emit(mutableListOf()) updateResultData.emit(mutableListOf())
} }
} }
@ -226,13 +227,14 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
} }
suspend fun cancelUpdateItemData(){ suspend fun cancelUpdateItemData(){
updateResultDataJob?.cancel() updateResultDataJob?.cancel(CancellationException())
updatingData.emit(false) updatingData.emit(false)
updatingData.value = false
updateResultData.emit(null) updateResultData.emit(null)
} }
suspend fun cancelUpdateFormatsItemData(){ suspend fun cancelUpdateFormatsItemData(){
updateFormatsResultDataJob?.cancel() updateFormatsResultDataJob?.cancel(CancellationException())
updatingFormats.emit(false) updatingFormats.emit(false)
updateFormatsResultData.emit(null) updateFormatsResultData.emit(null)
} }

View file

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

View file

@ -8,7 +8,6 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.ObserveSourceWorker import com.deniscerri.ytdlnis.work.ObserveSourceWorker
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit

View file

@ -10,9 +10,11 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PauseDownloadNotificationReceiver : BroadcastReceiver() { class PauseDownloadNotificationReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) { override fun onReceive(c: Context, intent: Intent) {
val result = goAsync()
val id = intent.getIntExtra("itemID", 0) val id = intent.getIntExtra("itemID", 0)
if (id != 0) { if (id != 0) {
runCatching { runCatching {
@ -20,13 +22,17 @@ class PauseDownloadNotificationReceiver : BroadcastReceiver() {
val notificationUtil = NotificationUtil(c) val notificationUtil = NotificationUtil(c)
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.createResumeDownload(id, title)
val dbManager = DBManager.getInstance(c) val dbManager = DBManager.getInstance(c)
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
runCatching { try {
val item = dbManager.downloadDao.getDownloadById(id.toLong()) val item = dbManager.downloadDao.getDownloadById(id.toLong())
item.status = DownloadRepository.Status.ActivePaused.toString() item.status = DownloadRepository.Status.ActivePaused.toString()
dbManager.downloadDao.update(item) 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.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -13,16 +12,10 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
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.Extensions.popup
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder( class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK DIFF_CALLBACK

View file

@ -130,12 +130,15 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
when(item.type) { when(item.type) {
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
btn.setIconResource(R.drawable.ic_music) btn.setIconResource(R.drawable.ic_music)
btn.contentDescription = activity.getString(R.string.audio)
} }
DownloadViewModel.Type.video -> { DownloadViewModel.Type.video -> {
btn.setIconResource(R.drawable.ic_video) btn.setIconResource(R.drawable.ic_video)
btn.contentDescription = activity.getString(R.string.video)
} }
else -> { else -> {
btn.setIconResource(R.drawable.ic_terminal) 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) val downloadTypeIcon = card.findViewById<MaterialButton>(R.id.download_type)
when(item.downloadType){ when(item.downloadType){
DownloadViewModel.Type.audio -> downloadTypeIcon.setIconResource(R.drawable.ic_music) DownloadViewModel.Type.audio -> {
DownloadViewModel.Type.video -> downloadTypeIcon.setIconResource(R.drawable.ic_video) downloadTypeIcon.setIconResource(R.drawable.ic_music)
DownloadViewModel.Type.command -> downloadTypeIcon.setIconResource(R.drawable.ic_terminal) 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 -> {} else -> {}
} }

View file

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

View file

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

View file

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

View file

@ -1,11 +1,9 @@
package com.deniscerri.ytdlnis.ui.adapter package com.deniscerri.ytdlnis.ui.adapter
import android.app.Activity import android.app.Activity
import android.graphics.Color
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.chip.Chip
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.squareup.picasso.Picasso
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -139,7 +136,6 @@ class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity:
} }
}else{ }else{
info.isVisible = true info.isVisible = true
checkMissing.isVisible = true
btn.setImageResource(R.drawable.ic_search) btn.setImageResource(R.drawable.ic_search)
btn.setOnClickListener { btn.setOnClickListener {

View file

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

View file

@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.compose.ui.util.fastJoinToString
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -14,7 +13,6 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.util.Extensions.popup import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView 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.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel 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.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View file

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

View file

@ -28,10 +28,10 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
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.setTextAndRecalculateWidth
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration 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.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment

View file

@ -15,7 +15,6 @@ import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -23,7 +22,6 @@ import androidx.core.view.setPadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R 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.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -83,7 +80,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) { downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null){ if (currentDownloadItem != null){
@ -304,12 +300,22 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
bottomSheet.show(parentFragmentManager, "cutVideoSheet") bottomSheet.show(parentFragmentManager, "cutVideoSheet")
} }
}, },
updateDataClicked = { cutDisabledClicked = {
CoroutineScope(Dispatchers.IO).launch { val isUpdatingData = ViewModelProvider(requireActivity())[ResultViewModel::class.java].updatingData.value
resultItem?.apply { if(isUpdatingData){
val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java] val snack = Snackbar.make(view, context.getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
rsVM.updateItemData(this) 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 = { extraCommandsClicked = {
@ -328,6 +334,16 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}catch (e : Exception){ }catch (e : Exception){
e.printStackTrace() 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.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
@ -19,7 +20,11 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.core.view.isVisible 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.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -139,6 +144,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
tabLayout = view.findViewById(R.id.download_tablayout) tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager) viewPager2 = view.findViewById(R.id.download_viewpager)
updateItem = view.findViewById(R.id.update_item) updateItem = view.findViewById(R.id.update_item)
viewPager2.isUserInputEnabled = sharedPreferences.getBoolean("swipe_gestures_download_card", true)
//loading shimmers //loading shimmers
@ -307,12 +313,31 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
download.isEnabled = false download.isEnabled = false
val item: DownloadItem = getDownloadItem() val item: DownloadItem = getDownloadItem()
item.downloadStartTime = it.timeInMillis item.downloadStartTime = it.timeInMillis
runBlocking { if (item.videoPreferences.alsoDownloadAsAudio){
downloadViewModel.queueDownloads(listOf(item)) val itemsToQueue = mutableListOf<DownloadItem>()
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime) itemsToQueue.add(item)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
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 { download!!.setOnClickListener {
@ -324,10 +349,24 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
val item: DownloadItem = getDownloadItem() val item: DownloadItem = getDownloadItem()
runBlocking { if (item.videoPreferences.alsoDownloadAsAudio){
downloadViewModel.queueDownloads(listOf(item)) 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){ return when(selectedTabPosition){
0 -> { 0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment 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(){ private fun updateWhenSwitching(){
val prevDownloadItem = getDownloadItem( val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1 if (viewPager2.currentItem == 1) 0 else 1
@ -591,6 +657,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
kotlin.runCatching { kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
f.updateSelectedAudioFormat(getDownloadItem(1).videoPreferences.audioFormatIDs.first())
} }
} }
1 -> { 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.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.UiUtil.populateCommandCard import com.deniscerri.ytdlnis.util.UiUtil.populateCommandCard
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File

View file

@ -10,8 +10,6 @@ import android.content.res.Configuration
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -30,15 +28,10 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format 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.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
@ -48,8 +41,6 @@ import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.work.UpdatePlaylistFormatsWorker
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@ -63,7 +54,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -80,6 +70,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var bottomAppBar: BottomAppBar private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView private lateinit var filesize : TextView
private lateinit var count : TextView
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -135,6 +126,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button) val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
filesize = view.findViewById(R.id.filesize) filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count)
scheduleBtn.setOnClickListener{ scheduleBtn.setOnClickListener{
@ -406,7 +398,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomSheet.dismiss() bottomSheet.dismiss()
}, },
cutClicked = {}, cutClicked = {},
updateDataClicked = {}, cutDisabledClicked = {},
extraCommandsClicked = { extraCommandsClicked = {
val callback = object : ExtraCommandsListener { val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) { override fun onChangeExtraCommand(c: String) {
@ -476,7 +468,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomSheet.dismiss() bottomSheet.dismiss()
}, },
cutClicked = {}, cutClicked = {},
updateDataClicked = {}, cutDisabledClicked = {},
filenameTemplateSet = { checked -> filenameTemplateSet = { checked ->
items.forEach { it.customFileNameTemplate = checked } items.forEach { it.customFileNameTemplate = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } } CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
@ -497,6 +489,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
items.forEach { it.videoPreferences.removeAudio = checked } items.forEach { it.videoPreferences.removeAudio = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } } CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
}, },
alsoDownloadAsAudioClicked = {},
extraCommandsClicked = { extraCommandsClicked = {
val callback = object : ExtraCommandsListener { val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) { override fun onChangeExtraCommand(c: String) {
@ -536,6 +529,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest { downloadViewModel.processingDownloads.collectLatest {
count.text = "${it.size} ${getString(R.string.selected)}"
listAdapter.submitList(it) listAdapter.submitList(it)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
updateFileSize(it.map { it2 -> it2.format.filesize }) updateFileSize(it.map { it2 -> it2.format.filesize })
@ -575,7 +569,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
} }
private fun updateFileSize(items: List<Long>){ 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.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM
import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
@ -322,12 +323,22 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
bottomSheet.show(parentFragmentManager, "cutVideoSheet") bottomSheet.show(parentFragmentManager, "cutVideoSheet")
} }
}, },
updateDataClicked = { cutDisabledClicked = {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { val isUpdatingData = ViewModelProvider(requireActivity())[ResultViewModel::class.java].updatingData.value
resultItem?.apply { if(isUpdatingData){
val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java] val snack = Snackbar.make(view, context.getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
rsVM.updateItemData(this) 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 = { filenameTemplateSet = {
@ -346,6 +357,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
downloadItem.videoPreferences.removeAudio = it downloadItem.videoPreferences.removeAudio = it
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, if (it) listOf() else downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }) 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 = { extraCommandsClicked = {
val callback = object : ExtraCommandsListener { val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) { override fun onChangeExtraCommand(c: String) {

View file

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

View file

@ -7,8 +7,6 @@ import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -16,23 +14,20 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.Button import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.ui.text.capitalize
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.materialswitch.MaterialSwitch 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 com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.w3c.dom.Text
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Month import java.time.Month
import java.time.format.TextStyle
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -102,6 +93,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var endsOnTime: TextInputLayout private lateinit var endsOnTime: TextInputLayout
private lateinit var endsAfterNr: TextInputLayout private lateinit var endsAfterNr: TextInputLayout
private lateinit var retryMissingDownloads: MaterialSwitch private lateinit var retryMissingDownloads: MaterialSwitch
private lateinit var getOnlyNewUploads: MaterialSwitch
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -114,13 +106,13 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
type = arguments?.getSerializable("type") as Type
currentItem = if (Build.VERSION.SDK_INT >= 33){ currentItem = if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelable("item", ObserveSourcesItem::class.java) arguments?.getParcelable("item", ObserveSourcesItem::class.java)
}else{ }else{
arguments?.getParcelable<ObserveSourcesItem>("item") arguments?.getParcelable<ObserveSourcesItem>("item")
} }
type = currentItem?.downloadItemTemplate?.type ?: arguments?.getSerializable("type") as Type
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -259,6 +251,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfter = view.findViewById(R.id.after) endsAfter = view.findViewById(R.id.after)
endsAfterNr = view.findViewById(R.id.after_nr) endsAfterNr = view.findViewById(R.id.after_nr)
retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads) retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads)
getOnlyNewUploads = view.findViewById(R.id.get_new_uploads)
okButton = view.findViewById(R.id.okButton) okButton = view.findViewById(R.id.okButton)
title.editText!!.setText(currentItem?.name ?: "") title.editText!!.setText(currentItem?.name ?: "")
@ -451,6 +444,9 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfter.performClick() endsAfter.performClick()
} }
retryMissingDownloads.isChecked = currentItem?.retryMissingDownloads ?: false
getOnlyNewUploads.isChecked = currentItem?.getOnlyNewUploads ?: false
if (currentItem != null) okButton.text = getString(R.string.update) if (currentItem != null) okButton.text = getString(R.string.update)
okButton.setOnClickListener { okButton.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
@ -483,6 +479,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
ends, ends,
endsAfterCount, endsAfterCount,
0, 0,
getOnlyNewUploads.isChecked,
retryMissingDownloads.isChecked, retryMissingDownloads.isChecked,
mutableListOf() mutableListOf()
) )

View file

@ -19,13 +19,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.WindowManager import android.widget.Button
import android.widget.* import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
@ -39,13 +39,13 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R 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.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel 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.Extensions.setFullScreen
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
@ -71,7 +71,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Calendar
import java.util.Locale
class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener { 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.ActionBar.LayoutParams
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -19,13 +18,11 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R 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.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity import com.deniscerri.ytdlnis.ui.adapter.PlaylistAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar

View file

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

View file

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

View file

@ -1,13 +1,9 @@
package com.deniscerri.ytdlnis.ui.downloads package com.deniscerri.ytdlnis.ui.downloads
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter 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 ( class DownloadListFragmentAdapter (
fragmentManager : FragmentManager, fragmentManager : FragmentManager,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,12 +32,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.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.DBManager.SORTING
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport 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.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.content.Context.INPUT_METHOD_SERVICE import android.content.Context.INPUT_METHOD_SERVICE
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@ -63,6 +64,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
return inflater.inflate(R.layout.fragment_cookies, container, false) return inflater.inflate(R.layout.fragment_cookies, container, false)
} }
@SuppressLint("RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
topAppBar = view.findViewById(R.id.logs_toolbar) topAppBar = view.findViewById(R.id.logs_toolbar)
@ -164,7 +166,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
private fun showDialog(url: String){ private fun showDialog(url: String){
lifecycleScope.launch { lifecycleScope.launch {
var item = withContext(Dispatchers.IO){ val item = withContext(Dispatchers.IO){
cookiesViewModel.getByURL(url) cookiesViewModel.getByURL(url)
} }
@ -178,11 +180,15 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
editText.setText(text) editText.setText(text)
editText.setSelection(editText.text.length) editText.setSelection(editText.text.length)
builder.setView(inputLayout) builder.setView(inputLayout)
builder.setNeutralButton(
getString(android.R.string.copy) item?.apply {
) { dialog: DialogInterface?, which: Int -> builder.setNeutralButton(
UiUtil.copyToClipboard(item.content, requireActivity()) getString(android.R.string.copy)
) { dialog: DialogInterface?, which: Int ->
UiUtil.copyToClipboard(item.content, requireActivity())
}
} }
builder.setPositiveButton( builder.setPositiveButton(
getString(R.string.get_cookies) getString(R.string.get_cookies)
) { dialog: DialogInterface?, which: Int -> ) { dialog: DialogInterface?, which: Int ->

View file

@ -4,23 +4,24 @@ import android.annotation.SuppressLint
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem 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.appbar.MaterialToolbar
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -73,13 +74,8 @@ class ObserveSourcesFragment : Fragment(), ObserveSourcesAdapter.OnItemClickList
) )
val newSource = view.findViewById<Chip>(R.id.newSource) val newSource = view.findViewById<Chip>(R.id.newSource)
recyclerView = view.findViewById(R.id.source_recyclerView) recyclerView = view.findViewById(R.id.source_recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
recyclerView.adapter = listAdapter 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] observeSourcesViewModel = ViewModelProvider(this)[ObserveSourcesViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
@ -163,63 +159,4 @@ class ObserveSourcesFragment : Fragment(), ObserveSourcesAdapter.OnItemClickList
observeSourcesViewModel.delete(item) 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.AccompanistWebViewClient
import com.google.accompanist.web.WebView import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState 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.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -57,48 +58,58 @@ class WebViewActivity : BaseActivity() {
cookieManager = CookieManager.getInstance() cookieManager = CookieManager.getInstance()
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)
webViewClient = object : AccompanistWebViewClient() { lifecycleScope.launch{
override fun onPageFinished(view: WebView?, url: String?) { webViewClient = object : AccompanistWebViewClient() {
super.onPageFinished(view, url) override fun onPageFinished(view: WebView?, url: String?) {
kotlin.runCatching { super.onPageFinished(view, url)
toolbar.title = view?.title ?: "" kotlin.runCatching {
cookies = CookieManager.getInstance().getCookie(view?.url) 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()
} }
}.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") @SuppressLint("SetJavaScriptEnabled")

View file

@ -15,6 +15,7 @@ import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.children
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -168,10 +169,12 @@ class DownloadLogFragment : Fragment() {
requireActivity().runOnUiThread{ requireActivity().runOnUiThread{
if (logItem != null){ if (logItem != null){
if (logItem.content.isNotBlank()) { 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 androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.DownloadLogsAdapter
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel 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.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode import com.deniscerri.ytdlnis.util.Extensions.forceFastScrollMode
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
@ -21,6 +22,7 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.updating override val title: Int = R.string.updating
private var updateYTDL: Preference? = null private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null private var ytdlVersion: Preference? = null
private var ytdlSource: Preference? = null
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
private var version: Preference? = null private var version: Preference? = null
@ -34,52 +36,22 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
val editor = preferences.edit() val editor = preferences.edit()
updateYTDL = findPreference("update_ytdl") updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version") ytdlVersion = findPreference("ytdl-version")
ytdlSource = findPreference("ytdlp_source")
YoutubeDL.getInstance().version(context)?.let { YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it) editor.putString("ytdl-version", it)
editor.apply() editor.apply()
ytdlVersion!!.summary = it ytdlVersion!!.summary = it
} }
ytdlSource?.setOnPreferenceChangeListener { preference, newValue ->
initYTDLUpdate(editor)
true
}
updateYTDL!!.onPreferenceClickListener = updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
lifecycleScope.launch { initYTDLUpdate(editor)
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()
}
}
true 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.recyclerview.widget.RecyclerView
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.TerminalItem import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdlnis.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator 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.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel 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.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight
import com.deniscerri.ytdlnis.util.Extensions.setCustomTextSize
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -228,6 +228,8 @@ class TerminalFragment : Fragment() {
input!!.requestFocus() input!!.requestFocus()
input!!.setSelection(input!!.text.length) input!!.setSelection(input!!.text.length)
output?.text = bundle?.getString("output") ?: output?.text output?.text = bundle?.getString("output") ?: output?.text
output?.isVisible = output?.text.toString().isNotEmpty()
if (bundle?.getBoolean("run") == true){ if (bundle?.getBoolean("run") == true){
showCancelFab() showCancelFab()
} }
@ -326,6 +328,7 @@ class TerminalFragment : Fragment() {
requireActivity().runOnUiThread{ requireActivity().runOnUiThread{
if (it != null){ if (it != null){
if (!it.log.isNullOrBlank()) { if (!it.log.isNullOrBlank()) {
output?.isVisible = true
output?.text = it.log output?.text = it.log
} }
output?.scrollTo(0, output!!.height) 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.EditText
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog 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.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme import com.neo.highlight.util.scheme.ColorScheme
@ -205,8 +202,6 @@ object Extensions {
format.substring(3) else if (this < 36000) format = format.substring(1) format.substring(3) else if (this < 36000) format = format.substring(1)
return format return format
} }
fun View.popup(){ fun View.popup(){
val animator = ValueAnimator.ofFloat( 0.75f, 1f) val animator = ValueAnimator.ofFloat( 0.75f, 1f)
animator.addUpdateListener { animation: ValueAnimator -> animator.addUpdateListener { animation: ValueAnimator ->
@ -219,6 +214,25 @@ object Extensions {
animator.start() 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()) } fun List<String>.closestValue(value: String) = minBy { abs(value.toInt() - it.toInt()) }
class CustomInterpolator : Interpolator { class CustomInterpolator : Interpolator {

View file

@ -35,6 +35,7 @@ import java.io.InputStreamReader
import java.lang.reflect.Type import java.lang.reflect.Type
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.regex.Pattern 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 { return runCatching {
when(sharedPreferences.getString("search_engine", "ytsearch")){ when(sharedPreferences.getString("search_engine", "ytsearch")){
"ytsearch" -> searchFromPiped(query) "ytsearch" -> searchFromPiped(query)
@ -66,8 +67,8 @@ class InfoUtil(private val context: Context) {
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun searchFromPiped(query: String): ArrayList<ResultItem?> { fun searchFromPiped(query: String): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem>()
val data = genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCODE}") val data = genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCODE}")
val dataArray = data.getJSONArray("items") val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query) if (dataArray.length() == 0) return getFromYTDL(query)
@ -85,8 +86,8 @@ class InfoUtil(private val context: Context) {
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun searchFromPipedMusic(query: String): ArrayList<ResultItem?> { fun searchFromPipedMusic(query: String): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem>()
val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCODE}") val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCODE}")
val dataArray = data.getJSONArray("items") val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query) if (dataArray.length() == 0) return getFromYTDL(query)
@ -106,7 +107,7 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple { fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
try{ try{
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem>()
// -------------- PIPED API FUNCTION ------------------- // -------------- PIPED API FUNCTION -------------------
var url = "" var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id" url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
@ -120,10 +121,11 @@ class InfoUtil(private val context: Context) {
for (i in 0 until dataArray.length()){ for (i in 0 until dataArray.length()){
kotlin.runCatching { kotlin.runCatching {
val obj = dataArray.getJSONObject(i) val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url")) createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))?.apply {
itm?.playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName } playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id" playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(itm) items.add(this)
}
} }
} }
if (nextpage == "null") nextpage = "" if (nextpage == "null") nextpage = ""
@ -137,17 +139,25 @@ class InfoUtil(private val context: Context) {
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun getYoutubeVideo(url: String): List<ResultItem?> { fun getYoutubeVideo(url: String): List<ResultItem>? {
val theURL = url.replace("\\?list.*".toRegex(), "") val theURL = url.replace("\\?list.*".toRegex(), "")
return try { try{
val id = getIDFromYoutubeURL(theURL) val id = getIDFromYoutubeURL(theURL)
val res = genericRequest("$pipedURL/streams/$id") 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){ }catch (e: Exception){
val v = getFromYTDL(theURL) val v = getFromYTDL(theURL)
v.forEach { it!!.url = theURL } v.forEach { it.url = theURL }
v return v
} }
return null
} }
private fun createVideofromJSON(obj: JSONObject): ResultItem? { private fun createVideofromJSON(obj: JSONObject): ResultItem? {
@ -359,7 +369,7 @@ class InfoUtil(private val context: Context) {
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun getTrending(): ArrayList<ResultItem?> { fun getTrending(): ArrayList<ResultItem> {
if (sharedPreferences.getString("api_key", "")!!.isNotBlank()){ if (sharedPreferences.getString("api_key", "")!!.isNotBlank()){
return getTrendingFromYoutubeAPI() return getTrendingFromYoutubeAPI()
} }
@ -367,8 +377,8 @@ class InfoUtil(private val context: Context) {
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun getTrendingFromYoutubeAPI(): ArrayList<ResultItem?> { fun getTrendingFromYoutubeAPI(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem>()
val key = sharedPreferences.getString("api_key", "")!! 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" val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10&regionCode=$countryCODE&maxResults=25&key=$key"
//short data //short data
@ -397,8 +407,8 @@ class InfoUtil(private val context: Context) {
return items return items
} }
private fun getTrendingFromPiped(): ArrayList<ResultItem?> { private fun getTrendingFromPiped(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem>()
val url = "$pipedURL/trending?region=$countryCODE" val url = "$pipedURL/trending?region=$countryCODE"
val res = genericArrayRequest(url) val res = genericArrayRequest(url)
for (i in 0 until res.length()) { for (i in 0 until res.length()) {
@ -438,7 +448,7 @@ class InfoUtil(private val context: Context) {
return try { return try {
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id") 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") val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
item!!.formats item!!.formats
}catch(e: Exception) { }catch(e: Exception) {
@ -571,8 +581,8 @@ class InfoUtil(private val context: Context) {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun getFromYTDL(query: String): ArrayList<ResultItem?> { fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch") val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
val request : YoutubeDLRequest val request : YoutubeDLRequest
@ -596,7 +606,7 @@ class InfoUtil(private val context: Context) {
} }
request.addOption("--flat-playlist") request.addOption("--flat-playlist")
request.addOption("-j") request.addOption(if (singleItem) "-J" else "-j")
request.addOption("--skip-download") request.addOption("--skip-download")
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--compat-options", "manifest-filesize-approx") 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){ val url = if (jsonObject.has("url") && results.size > 1){
jsonObject.getString("url") jsonObject.getString("url")
}else{ }else{
if (Patterns.WEB_URL.matcher(query).matches()){ if (Patterns.WEB_URL.matcher(query).matches() && playlistURL?.isEmpty() == true){
query query
}else{ }else{
jsonObject.getStringByAny("webpage_url", "original_url", "url", )
jsonObject.getString("webpage_url") jsonObject.getString("webpage_url")
} }
} }
items.add(ResultItem(0, val res = ResultItem(0,
url, url,
title, title,
author, author,
@ -715,7 +726,8 @@ class InfoUtil(private val context: Context) {
playlistURL, playlistURL,
playlistIndex playlistIndex
) )
)
items.add(res)
} }
return items return items
} }
@ -772,79 +784,6 @@ class InfoUtil(private val context: Context) {
return formats 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> { fun getSearchSuggestions(query: String): ArrayList<String> {
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query" 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) { if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail") request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", "jpg") request.addOption("--convert-thumbnails", sharedPreferences.getString("thumbnail_format", "jpg")!!)
} }
if (!sharedPreferences.getBoolean("mtime", false)){ if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime") 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:(?P<uploader>.+)(?: - Topic)$")
request.addOption("--parse-metadata", "%(uploader)s:%(album_artist)s")
if (downloadItem.author.isNotBlank()){ if (embedMetadata){
request.addCommands(listOf("--replace-in-metadata", "uploader", ".+", downloadItem.author)) request.addOption("--parse-metadata", "%(uploader,channel,creator,artist|null)s:%(uploader)s")
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
} }
if (downloadItem.downloadSections.isNotBlank()){ if (downloadItem.downloadSections.isNotBlank()){
@ -1122,7 +1088,7 @@ class InfoUtil(private val context: Context) {
"%(section_title&{} |)s$filenameTemplate" "%(section_title&{} |)s$filenameTemplate"
} }
if (downloadItem.downloadSections.split(";").size > 1){ 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()){ if (downloadItem.extraCommands.isNotBlank()){
val cache = File(FileUtil.getCachePath(context)) val cache = File(FileUtil.getCachePath(context))
cache.mkdirs() cache.mkdirs()
val conf = File(cache.absolutePath + "/${System.currentTimeMillis()}.txt") val conf = File(cache.absolutePath + "/${System.currentTimeMillis()}${UUID.randomUUID()}.txt")
conf.createNewFile() conf.createNewFile()
conf.writeText(downloadItem.extraCommands) conf.writeText(downloadItem.extraCommands)
request.addOption( request.addOption(
@ -1146,16 +1112,9 @@ class InfoUtil(private val context: Context) {
request.addOption("--write-description") request.addOption("--write-description")
} }
filenameTemplate = filenameTemplate.replace("%(uploader)s", "%(uploader,channel)s") filenameTemplate = filenameTemplate
} .replace("%(uploader)s", "%(uploader,channel).30B")
.replace("%(title)s", "%(title).170B")
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")
}
}
} }
if (sharedPreferences.getBoolean("download_archive", false)){ if (sharedPreferences.getBoolean("download_archive", false)){
@ -1216,9 +1175,9 @@ class InfoUtil(private val context: Context) {
if (embedMetadata){ if (embedMetadata){
request.addOption("--embed-metadata") 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", "%(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()) { if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") 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( class PlaylistTuple internal constructor(
var nextPageToken: String, var nextPageToken: String,
var videos: ArrayList<ResultItem?> var videos: ArrayList<ResultItem>
) )
companion object { companion object {

View file

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

View file

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

View file

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

View file

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

View file

@ -18,16 +18,15 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await import androidx.work.await
import androidx.work.workDataOf import androidx.work.workDataOf
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
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.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration
import com.deniscerri.ytdlnis.util.Extensions.toStringDuration import com.deniscerri.ytdlnis.util.Extensions.toStringDuration
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
@ -36,6 +35,7 @@ import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -59,6 +59,7 @@ class DownloadWorker(
val historyDao = dbManager.historyDao val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao) val logRepo = LogRepository(dbManager.logDao)
val resultRepo = ResultRepository(dbManager.resultDao, context)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val alarmScheduler = AlarmScheduler(context) val alarmScheduler = AlarmScheduler(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -67,6 +68,8 @@ class DownloadWorker(
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() 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 confTmp = Configuration(context.resources.configuration)
val currLang = sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }.split("-") 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])) 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) val request = infoUtil.buildYoutubeDLRequest(downloadItem)
downloadItem.status = DownloadRepository.Status.Active.toString() downloadItem.status = DownloadRepository.Status.Active.toString()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
delay(1500)
//update item if its incomplete //update item if its incomplete
updateDownloadItem(downloadItem, infoUtil, dao, resultDao) resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
}
} }
val cacheDir = FileUtil.getCachePath(context) val cacheDir = FileUtil.getCachePath(context)
@ -164,7 +170,10 @@ class DownloadWorker(
} }
} }
}.onSuccess { }.onSuccess {
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
}
val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking { runBlocking {
var finalPaths : List<String>? var finalPaths : List<String>?
@ -178,12 +187,6 @@ class DownloadWorker(
.map { it.removeSuffix("'") } .map { it.removeSuffix("'") }
.sortedBy { File(it).lastModified() } .sortedBy { File(it).lastModified() }
.toList() .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) FileUtil.scanMedia(finalPaths, context)
if (finalPaths.isEmpty()){ if (finalPaths.isEmpty()){
finalPaths = listOf(context.getString(R.string.unfound_file)) 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) FileUtil.deleteConfigFiles(request)
//put download in history //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 downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources
) )
if (wasQuickDownloaded){ if (wasQuickDownloaded && createResultItem){
runCatching { runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id))
runBlocking { runBlocking {
@ -338,32 +348,6 @@ class DownloadWorker(
return Result.success() 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 { companion object {

View file

@ -6,27 +6,23 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.database.DBManager 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.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ObserveSourcesItem 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.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository import com.deniscerri.ytdlnis.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.receiver.ObserveAlarmReceiver 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.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Calendar import java.util.Calendar
@ -41,6 +37,7 @@ class ObserveSourceWorker(
if (sourceID == 0L) return Result.failure() if (sourceID == 0L) return Result.failure()
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao) val repo = ObserveSourcesRepository(dbManager.observeSourcesDao)
val resultsRepo = ResultRepository(dbManager.resultDao, App.instance)
val historyRepo = HistoryRepository(dbManager.historyDao) val historyRepo = HistoryRepository(dbManager.historyDao)
val downloadRepo = DownloadRepository(dbManager.downloadDao) val downloadRepo = DownloadRepository(dbManager.downloadDao)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -48,7 +45,7 @@ class ObserveSourceWorker(
val item = repo.getByID(sourceID) val item = repo.getByID(sourceID)
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){ if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){
DownloadUtil.cancelObservationTaskByID(context, item.id) repo.cancelObservationTaskByID(context, item.id)
return Result.failure() return Result.failure()
} }
@ -57,36 +54,50 @@ class ObserveSourceWorker(
val foregroundInfo = ForegroundInfo(workerID, notification) val foregroundInfo = ForegroundInfo(workerID, notification)
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
item.runCount = item.runCount + 1 val res = runCatching { infoUtil.getFromYTDL(item.url).toList() }.getOrElse { listOf() }
.filter { result ->
var res = runCatching { infoUtil.getFromYTDL(item.url).toList() }.getOrElse { listOf() } //if first run and get new items only is preferred then dont get anything on first run
res = if (!item.retryMissingDownloads){ if (item.getOnlyNewUploads && item.runCount == 0){
res.filter { result -> item.alreadyProcessedLinks.add(result.url)
//all items that are not present in history false
!historyRepo.getAllByURL(result!!.url).filter { hi -> }else{
hi.downloadPath.any { path -> FileUtil.exists(path) } true
}.map { it.url }.contains(result.url) }
} }
}else{ .filter { result ->
//all items that are not already processed
res.filter { !item.alreadyProcessedLinks.contains(it!!.url) }
}
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>() val items = mutableListOf<DownloadItem>()
res.forEach { res.forEach {
val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java) val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java)
val downloadItem = Gson().fromJson(string, 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 downloadItem.id = 0L
items.add(downloadItem) items.add(downloadItem)
} }
withContext(Dispatchers.IO){
repo.update(item)
}
if (items.isNotEmpty()){ if (items.isNotEmpty()){
@ -108,69 +119,73 @@ class ObserveSourceWorker(
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" } items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
} }
runBlocking { items.forEach {
items.forEach { if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString()
if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString() val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val currentCommand = infoUtil.buildYoutubeDLRequest(it) val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> d.id = 0
d.id = 0 d.logID = null
d.logID = null d.customFileNameTemplate = it.customFileNameTemplate
d.customFileNameTemplate = it.customFileNameTemplate d.status = DownloadRepository.Status.Queued.toString()
d.status = DownloadRepository.Status.Queued.toString() d.toString() == it.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) existing.add(it)
}else{ }else{
//check if downloaded and file exists if (it.id == 0L){
val history = withContext(Dispatchers.IO){ it.id = downloadRepo.insert(it)
historyRepo.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } } }else if (it.status == DownloadRepository.Status.Queued.toString()){
downloadRepo.update(it)
} }
val existingHistory = history.firstOrNull { queuedItems.add(it)
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistory != null){
it.id = existingHistory.id
existing.add(it)
}else{
if (it.id == 0L){
it.id = downloadRepo.insert(it)
}else if (it.status == DownloadRepository.Status.Queued.toString()){
downloadRepo.update(it)
}
queuedItems.add(it)
}
} }
} }
} }
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){ if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
DownloadUtil.startDownloadWorker(queuedItems, context) downloadRepo.startDownloadWorker(queuedItems, context, Data.Builder().putBoolean("createResultItem", false))
if(!useScheduler){ if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
try{ runCatching {
updateDownloadItem(it, infoUtil, downloadRepo, dbManager.resultDao) resultsRepo.updateDownloadItem(it)?.apply {
}catch (ignored: Exception){} downloadRepo.updateWithoutUpsert(this)
}
}
} }
}else{ }else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach { queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{ runCatching {
updateDownloadItem(it, infoUtil, downloadRepo, dbManager.resultDao) resultsRepo.updateDownloadItem(it)?.apply {
}catch (ignored: Exception){} downloadRepo.updateWithoutUpsert(this)
}
}
} }
} }
} }
item.alreadyProcessedLinks.removeAll(items.map { it.url })
item.alreadyProcessedLinks.addAll(items.map { it.url }) item.alreadyProcessedLinks.addAll(items.map { it.url })
} }
item.runCount = item.runCount + 1
if (item.runCount > item.endsAfterCount && item.endsAfterCount > 0){ if (item.runCount > item.endsAfterCount && item.endsAfterCount > 0){
item.status = ObserveSourcesRepository.SourceStatus.STOPPED item.status = ObserveSourcesRepository.SourceStatus.STOPPED
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
@ -185,10 +200,14 @@ class ObserveSourceWorker(
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
repo.update(item) repo.update(item)
} }
DownloadUtil.cancelObservationTaskByID(context, item.id) repo.cancelObservationTaskByID(context, item.id)
return Result.success() return Result.success()
} }
withContext(Dispatchers.IO){
repo.update(item)
}
scheduleForNextTime(item) scheduleForNextTime(item)
return Result.success() 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.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity import com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
@ -93,7 +92,7 @@ class TerminalDownloadWorker(
0, 0,
"Terminal Task", "Terminal Task",
"Terminal Task\n" + "Terminal Task\n" +
"Command: ${command}\n\n", "Command: \n ${command}\n\n",
Format(), Format(),
DownloadViewModel.Type.command, DownloadViewModel.Type.command,
System.currentTimeMillis(), System.currentTimeMillis(),

View file

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

View file

@ -1,5 +1,10 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> 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> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> 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> </vector>

View file

@ -1,6 +1,10 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> android:viewportWidth="960"
<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"/> 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> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> 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> </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