This commit is contained in:
zaednasr 2024-07-01 20:20:58 +02:00
parent 313358c4bb
commit 15a4ecf092
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
81 changed files with 3827 additions and 764 deletions

View file

@ -10,7 +10,7 @@ plugins {
def properties = new Properties()
def versionMajor = 1
def versionMinor = 7
def versionPatch = 7
def versionPatch = 8
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def isBeta = false
def versionExt = isBeta ? ".${versionBuild}-beta" : ""
@ -130,6 +130,10 @@ dependencies {
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
//
// implementation "io.github.junkfood02.youtubedl-android:library:0.16.0"
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.0"
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.0"
implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -142,6 +146,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5'
implementation 'androidx.compose.ui:ui-android:1.6.5'
implementation 'androidx.preference:preference:1.2.1'
testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -177,7 +182,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer

View file

@ -0,0 +1,732 @@
{
"formatVersion": 1,
"database": {
"version": 18,
"identityHash": "0e5e77e8657b2b151e63613f8d6f8110",
"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": "sources",
"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, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyTime` INTEGER NOT NULL, `weeklyConfig` TEXT, `monthlyConfig` TEXT, `status` TEXT NOT NULL, `startsTime` INTEGER 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, `ignoredLinks` TEXT NOT NULL DEFAULT '[]', `alreadyProcessedLinks` TEXT NOT NULL, `syncWithSource` INTEGER NOT NULL DEFAULT 0)",
"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": "everyNr",
"columnName": "everyNr",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "everyCategory",
"columnName": "everyCategory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyTime",
"columnName": "everyTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "weeklyConfig",
"columnName": "weeklyConfig",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "monthlyConfig",
"columnName": "monthlyConfig",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "startsTime",
"columnName": "startsTime",
"affinity": "INTEGER",
"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": "ignoredLinks",
"columnName": "ignoredLinks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "alreadyProcessedLinks",
"columnName": "alreadyProcessedLinks",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "syncWithSource",
"columnName": "syncWithSource",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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, '0e5e77e8657b2b151e63613f8d6f8110')"
]
}
}

View file

@ -0,0 +1,739 @@
{
"formatVersion": 1,
"database": {
"version": 19,
"identityHash": "348500cc087b2ef2e8f43470db15069e",
"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, `incognito` INTEGER NOT NULL DEFAULT 0)",
"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
},
{
"fieldPath": "incognito",
"columnName": "incognito",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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": "sources",
"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, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyTime` INTEGER NOT NULL, `weeklyConfig` TEXT, `monthlyConfig` TEXT, `status` TEXT NOT NULL, `startsTime` INTEGER 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, `ignoredLinks` TEXT NOT NULL DEFAULT '[]', `alreadyProcessedLinks` TEXT NOT NULL, `syncWithSource` INTEGER NOT NULL DEFAULT 0)",
"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": "everyNr",
"columnName": "everyNr",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "everyCategory",
"columnName": "everyCategory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyTime",
"columnName": "everyTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "weeklyConfig",
"columnName": "weeklyConfig",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "monthlyConfig",
"columnName": "monthlyConfig",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "startsTime",
"columnName": "startsTime",
"affinity": "INTEGER",
"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": "ignoredLinks",
"columnName": "ignoredLinks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "alreadyProcessedLinks",
"columnName": "alreadyProcessedLinks",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "syncWithSource",
"columnName": "syncWithSource",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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, '348500cc087b2ef2e8f43470db15069e')"
]
}
}

View file

@ -42,9 +42,13 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.HomeFragment
import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment
import com.deniscerri.ytdl.ui.downloads.HistoryFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.util.CrashListener
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.bottomnavigation.BottomNavigationView
@ -125,26 +129,39 @@ class MainActivity : BaseActivity() {
}
}
if (savedInstanceState == null){
val graph = navController.navInflater.inflate(R.navigation.nav_graph)
graph.setStartDestination(R.id.homeFragment)
when(preferences.getString("start_destination", "")) {
"History" -> graph.setStartDestination(R.id.historyFragment)
"More" -> if (navigationView is NavigationBarView) graph.setStartDestination(R.id.moreFragment)
}
navController.graph = graph
}
NavbarUtil.init(this)
if (navigationView is NavigationBarView){
if (savedInstanceState == null){
val graph = navController.navInflater.inflate(R.navigation.nav_graph)
graph.setStartDestination(NavbarUtil.getStartFragmentId(this))
navController.graph = graph
}
(navigationView as NavigationBarView).applyNavBarStyle()
val showingDownloadQueue = NavbarUtil.getNavBarItems(this).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
(navigationView as NavigationBarView).setupWithNavController(navController)
(navigationView as NavigationBarView).setOnItemReselectedListener {
when (it.itemId) {
R.id.homeFragment -> {
(navHostFragment.childFragmentManager.primaryNavigationFragment!! as HomeFragment).scrollToTop()
kotlin.runCatching {
(navHostFragment.childFragmentManager.primaryNavigationFragment!! as HomeFragment).scrollToTop()
}
}
R.id.historyFragment -> {
navController.navigate(R.id.downloadQueueMainFragment)
if(!showingDownloadQueue) {
navController.navigate(R.id.downloadQueueMainFragment)
}else{
kotlin.runCatching {
(navHostFragment.childFragmentManager.primaryNavigationFragment!! as HistoryFragment).scrollToTop()
}
}
}
R.id.downloadQueueMainFragment -> {
kotlin.runCatching {
(navHostFragment.childFragmentManager.primaryNavigationFragment!! as DownloadQueueMainFragment).scrollToActive()
}
}
R.id.moreFragment -> {
val intent = Intent(context, SettingsActivity::class.java)
@ -153,7 +170,11 @@ class MainActivity : BaseActivity() {
}
}
val activeDownloadsBadge = (navigationView as NavigationBarView).getOrCreateBadge(R.id.historyFragment)
val activeDownloadsBadge = if (showingDownloadQueue) {
(navigationView as NavigationBarView).getOrCreateBadge(R.id.downloadQueueMainFragment)
}else{
(navigationView as NavigationBarView).getOrCreateBadge(R.id.historyFragment)
}
lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest {
if (it == 0) {
@ -220,12 +241,14 @@ class MainActivity : BaseActivity() {
(navigationView as NavigationView).getHeaderView(0).findViewById<TextView>(R.id.title).text = ThemeUtil.getStyledAppName(this)
}
val showingNavbarItems = NavbarUtil.getNavBarItems(this).filter { it.isVisible }.map { it.itemId }
navController.addOnDestinationChangedListener { _, destination, _ ->
Handler(Looper.getMainLooper()).post {
if (navigationView is NavigationBarView){
when(destination.id){
R.id.homeFragment, R.id.historyFragment, R.id.moreFragment -> showBottomNavigation()
else -> hideBottomNavigation()
if (showingNavbarItems.contains(destination.id)) {
showBottomNavigation()
}else{
hideBottomNavigation()
}
}
}

View file

@ -41,7 +41,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem
TerminalItem::class,
ObserveSourcesItem::class
],
version = 17,
version = 19,
autoMigrations = [
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3),
@ -58,7 +58,9 @@ import com.deniscerri.ytdl.database.models.TerminalItem
// AutoMigration (from = 13, to = 14) MANUALLY HANDLED
AutoMigration (from = 14, to = 15),
AutoMigration (from = 15, to = 16, spec = Migrations.resetObserveSources::class),
AutoMigration (from = 16, to = 17)
AutoMigration (from = 16, to = 17),
AutoMigration (from = 17, to = 18),
AutoMigration (from = 18, to = 19),
]
)
abstract class DBManager : RoomDatabase(){

View file

@ -23,7 +23,11 @@ object Migrations {
}
database.execSQL("CREATE TABLE IF NOT EXISTS `observeSources` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `status` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyWeekDay` TEXT NOT NULL, `everyMonthDay` INTEGER NOT NULL, `everyTime` INTEGER NOT NULL, `startsTime` INTEGER NOT NULL, `startsMonth` TEXT NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `alreadyProcessedLinks` TEXT NOT NULL)")
}
},
// Migration(17, 18 ){ database ->
// database.execSQL("ALTER TABLE `sources` ADD COLUMN `syncWithSource` INTEGER NOT NULL DEFAULT 0")
// }
)
@DeleteTable.Entries(
@ -39,5 +43,4 @@ object Migrations {
}
}

View file

@ -83,6 +83,9 @@ interface CommandTemplateDao {
@Query("DELETE FROM templateShortcuts WHERE id=:itemId")
suspend fun deleteShortcut(itemId: Long)
@Query("DELETE FROM templateShortcuts")
suspend fun deleteAllShortcuts()
@Update
suspend fun update(item: CommandTemplate)
}

View file

@ -50,8 +50,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem>
@ -63,12 +61,12 @@ interface DownloadDao {
suspend fun updateProcessingDownloadPath(path: String)
@Query("SELECT * FROM downloads WHERE status='Active'")
suspend fun getActiveDownloadsList() : List<DownloadItem>
fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status in ('Active', 'QueuedPaused', 'ActivePaused')")
suspend fun resetActivePausedItems()
@Query("UPDATE downloads SET status='Queued' where status = 'Active'")
suspend fun resetActiveToQueued()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued')")
fun getActiveAndQueuedDownloadIDs() : List<Long>
@ -102,8 +100,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple>
@ -159,6 +155,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Error'")
suspend fun deleteErrored()
@Query("DELETE FROM downloads WHERE status='Queued'")
suspend fun deleteQueued()
@Query("DELETE FROM downloads WHERE status='Saved'")
suspend fun deleteSaved()
@ -269,10 +268,16 @@ interface DownloadDao {
@Query("Update downloads set id=:newId where id=:id")
suspend fun updateDownloadID(id: Long, newId: Long)
@Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in (:statuses) ORDER BY id DESC")
fun getIDsBetweenTwoItems(item1: Long, item2: Long, statuses: List<String>) : List<Long>
@Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Scheduled') ORDER BY downloadStartTime, id")
fun getScheduledIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long>
@Query("UPDATE downloads set incognito=:incognito WHERE status='Processing'")
suspend fun updateProcessingIncognito(incognito: Boolean)
@Query("SELECT COUNT(id) FROM downloads WHERE status='Processing' AND incognito='1'")
fun getProcessingAsIncognitoCount(): Int
}

View file

@ -42,5 +42,7 @@ data class DownloadItem(
@ColumnInfo(defaultValue = "")
var playlistURL: String? = "",
@ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null
var playlistIndex: Int? = null,
@ColumnInfo(defaultValue = "0")
var incognito: Boolean = false
) : Parcelable

View file

@ -22,4 +22,5 @@ data class DownloadItemSimple(
var type: DownloadViewModel.Type,
@ColumnInfo(defaultValue = "0")
var downloadStartTime: Long,
var incognito: Boolean = false
)

View file

@ -0,0 +1,19 @@
package com.deniscerri.ytdl.database.models
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.google.gson.JsonArray
data class RestoreAppDataItem(
var settings : JsonArray? = null,
var downloads: List<HistoryItem>? = null,
var queued: List<DownloadItem>? = null,
var scheduled: List<DownloadItem>? = null,
var cancelled: List<DownloadItem>? = null,
var errored: List<DownloadItem>? = null,
var saved: List<DownloadItem>? = null,
var cookies: List<CookieItem>? = null,
var templates: List<CommandTemplate>? = null,
var shortcuts: List<TemplateShortcut>? = null,
var searchHistory: List<SearchHistoryItem>? = null,
var observeSources: List<ObserveSourcesItem>? = null,
)

View file

@ -37,5 +37,7 @@ data class ObserveSourcesItem(
var retryMissingDownloads: Boolean,
@ColumnInfo(defaultValue = "[]")
var ignoredLinks: MutableList<String>,
var alreadyProcessedLinks : MutableList<String>
var alreadyProcessedLinks : MutableList<String>,
@ColumnInfo(defaultValue = "0")
var syncWithSource: Boolean
) : Parcelable

View file

@ -60,6 +60,10 @@ class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
commandDao.deleteShortcut(item.id)
}
suspend fun deleteAllShortcuts() {
commandDao.deleteAllShortcuts()
}
suspend fun deleteAll(){
commandDao.deleteAll()
}

View file

@ -73,7 +73,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status {
Active, ActivePaused, Queued, Error, Cancelled, Saved, Processing, Scheduled
Active, Queued, Error, Cancelled, Saved, Processing, Scheduled
}
suspend fun insert(item: DownloadItem) : Long {
@ -118,12 +118,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getDownloadsByIds(ids)
}
fun getAllItemsByIDsFlow(ids: List<Long>) : Flow<List<DownloadItem>> {
return downloadDao.getDownloadsByIdsFlow(ids)
}
fun getActiveDownloads() : List<DownloadItem> {
return downloadDao.getActiveAndPausedDownloadsList()
return downloadDao.getActiveDownloadsList()
}
fun getProcessingDownloads() : List<DownloadItem> {
@ -154,10 +150,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getCancelledDownloadsList()
}
fun getPausedDownloads() : List<DownloadItem> {
return downloadDao.getPausedDownloadsList()
}
fun getErroredDownloads() : List<DownloadItem> {
return downloadDao.getErroredDownloadsList()
}
@ -182,6 +174,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
deleteCache(errored)
}
suspend fun deleteQueued() {
downloadDao.deleteQueued()
}
suspend fun deleteSaved(){
downloadDao.deleteSaved()
}
@ -238,7 +234,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
//workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
@ -268,12 +264,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}, 0)
}
val first = queuedItems.first()
if (first.downloadStartTime > 0L) {
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime)
handler.postDelayed({
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}, 0)
if (queuedItems.isNotEmpty()) {
val first = queuedItems.first()
if (first.downloadStartTime > 0L) {
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime)
handler.postDelayed({
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}, 0)
}
}
}

View file

@ -12,6 +12,7 @@ import java.util.regex.Pattern
class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
private val tag: String = "ResultRepository"
val YTDLNIS_SEARCH = "YTDLNIS_SEARCH"
val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1)
@ -56,7 +57,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll()
itemCount.value = v.size
}else{
v.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = "ytdlnis-Search" }
v.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = YTDLNIS_SEARCH }
}
if (addToResults){
val ids = resultDao.insertMultiple(v)
@ -99,7 +100,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll()
itemCount.value = items.size
}else{
items.filter { it.playlistTitle.isNullOrBlank() }.forEach { it.playlistTitle = "ytdlnis-Search" }
items.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = YTDLNIS_SEARCH }
}
if (addToResults){
@ -182,7 +183,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
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
if (downloadItem.playlistTitle.isEmpty() && downloadItem.playlistTitle != "ytdlnis-Search") downloadItem.playlistTitle = info.playlistTitle
if (downloadItem.playlistTitle.isNotBlank() && downloadItem.playlistTitle != YTDLNIS_SEARCH) downloadItem.playlistTitle = info.playlistTitle
downloadItem.duration = info.duration
downloadItem.website = info.website
if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info.thumb

View file

@ -111,6 +111,10 @@ class CommandTemplateViewModel(private val application: Application) : AndroidVi
repository.deleteShortcut(item)
}
fun deleteAllShortcuts() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAllShortcuts()
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}

View file

@ -76,21 +76,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>>
private var bestVideoFormat : Format
private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList<Format>
private val videoQualityPreference: String
private val formatIDPreference: List<String>
private val audioFormatIDPreference: List<String>
private val resources : Resources
private var extraCommandsForAudio: String = ""
private var extraCommandsForVideo: String = ""
private var audioContainer: String?
private var videoContainer: String?
private var videoCodec: String?
private var audioCodec: String?
private val dao: DownloadDao
private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository
@ -137,37 +125,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
}
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val confTmp = Configuration(application.resources.configuration)
confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!))
val metrics = DisplayMetrics()
resources = Resources(application.assets, metrics, confTmp)
videoContainer = sharedPreferences.getString("video_format", "Default")
defaultVideoFormats = infoUtil.getGenericVideoFormats(resources)
bestVideoFormat = defaultVideoFormats.first()
audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
infoUtil.getGenericAudioFormats(resources).first()
}else{
Format(
audioFormatIDPreference.first().split("+").first(),
audioContainer!!,
"",
"",
"",
0,
audioFormatIDPreference.first().split("+").first()
)
}
videoCodec = sharedPreferences.getString("video_codec", "")
audioCodec = sharedPreferences.getString("audio_codec", "")
}
fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) {
@ -325,14 +282,26 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
container,
"",
ArrayList(),
path, historyItem.website, "", "", audioPreferences, videoPreferences, extraCommands, customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Queued.toString(), 0, null
path,
historyItem.website,
"",
"",
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(),
0,
null,
incognito = sharedPreferences.getBoolean("incognito", false)
)
}
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
return sharedDownloadViewModel.getPreferredVideoRequirements()
return sharedDownloadViewModel.getPreferredAudioRequirements()
}
//requirement and importance
@ -371,7 +340,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
try {
itemIDs.forEachIndexed { idx, it ->
val item = repository.getItemByID(it)
if (!isActive) throw CancellationException()
if (processingItemsFlow?.job?.isCancelled == true) throw CancellationException()
item.id = 0
item.status = DownloadRepository.Status.Processing.toString()
@ -383,11 +352,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
processingItems.emit(false)
} catch (e: Exception) {
processingItemsFlow?.apply {
this.processingDownloadItemIDs.chunked(100).forEach {
repository.deleteAllWithIDs(it)
}
}
deleteProcessing()
updateProcessingJobData(null)
processingItems.emit(false)
}
@ -399,6 +364,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun turnResultItemsToProcessingDownloads(itemIDs: List<Long>, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
processingItems.emit(true)
try {
itemIDs.forEach { id ->
@ -409,7 +375,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
))
downloadItem.status = DownloadRepository.Status.Processing.toString()
if (!isActive) {
if (processingItemsFlow?.job?.isCancelled == true) {
throw CancellationException()
}
@ -425,11 +391,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
processingItems.emit(false)
}catch (e: Exception) {
processingItemsFlow?.apply {
this.processingDownloadItemIDs.chunked(100).forEach {
repository.deleteAllWithIDs(it)
}
}
deleteProcessing()
updateProcessingJobData(null)
processingItems.emit(false)
}
@ -472,6 +434,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteErrored()
}
fun deleteQueued() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteQueued()
}
fun deleteSaved() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteSaved()
}
@ -513,7 +479,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun getActiveDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).toListString())
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active).toListString())
}
fun getActiveQueuedDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active, DownloadRepository.Status.Queued).toListString())
}
fun getQueuedDownloadsCount() : Int {
@ -529,8 +499,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application)
}
suspend fun resetActivePaused() {
dbManager.downloadDao.resetActivePausedItems()
suspend fun resetActiveToQueued() {
dbManager.downloadDao.resetActiveToQueued()
}
suspend fun startDownloadWorker(list: List<DownloadItem>){
@ -751,5 +721,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getScheduledIDsBetweenTwoItems(item1, item2)
}
suspend fun updateProcessingIncognito(incognito: Boolean) {
dao.updateProcessingIncognito(incognito)
}
fun areAllProcessingIncognito() : Boolean {
return dao.getProcessingAsIncognitoCount() > 0
}
}

View file

@ -29,6 +29,7 @@ import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.google.gson.Gson
@ -230,13 +231,18 @@ class SharedDownloadViewModel(private val context: Context) {
resultItem.formats,
downloadPath!!, resultItem.website,
"",
resultItem.playlistTitle,
if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
DownloadRepository.Status.Queued.toString(),
0,
null,
playlistURL = resultItem.playlistURL,
playlistIndex = resultItem.playlistIndex,
incognito = sharedPreferences.getBoolean("incognito", false)
)
}
@ -281,7 +287,7 @@ class SharedDownloadViewModel(private val context: Context) {
}
private fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet()
@ -345,9 +351,8 @@ class SharedDownloadViewModel(private val context: Context) {
for(i in 0..preferenceIndex){
removeAt(0)
}
requirements.add { it: Format -> if (it.format_note.contains(preference, ignoreCase = true)) importance else 0 }
add(0, preference)
forEachIndexed { index, res ->
if (index >= 2) importance = 0
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 }
}
}
@ -381,8 +386,10 @@ class SharedDownloadViewModel(private val context: Context) {
return cloneFormat (
try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
val requirements = getPreferredAudioRequirements()
theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
FormatSorter(context).sortAudioFormats(theFormats).first()
//
// val requirements = getPreferredAudioRequirements()
// theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
}catch (e: Exception){
bestAudioFormat
}
@ -395,24 +402,27 @@ class SharedDownloadViewModel(private val context: Context) {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
defaultVideoFormats.sortedByDescending { it.filesize }
}
when (videoQualityPreference) {
"worst" -> {
theFormats.last()
}
else /*best*/ -> {
val requirements = getPreferredVideoRequirements()
theFormats.run {
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
}else{
sortedByDescending { it.filesize }.maxByOrNull { f ->
val summ = requirements.sumOf { req -> req(f) }
summ
} ?: throw Exception()
}
}
}
}
FormatSorter(context).sortVideoFormats(theFormats).first()
//
// when (videoQualityPreference) {
// "worst" -> {
// theFormats.last()
// }
// else /*best*/ -> {
// val requirements = getPreferredVideoRequirements()
// theFormats.run {
// if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
// sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
// }else{
// sortedByDescending { it.filesize }.maxByOrNull { f ->
// val summ = requirements.sumOf { req -> req(f) }
// summ
// } ?: throw Exception()
// }
// }
// }
// }
}catch (e: Exception){
bestVideoFormat
}

View file

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

View file

@ -122,16 +122,24 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)}
val activePaused = item.status == DownloadRepository.Status.ActivePaused.toString()
val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume)
resumeButton.isVisible = activePaused
if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null)
resumeButton.setOnClickListener {
resumeButton.isVisible = false
onItemClickListener.onResumeClick(item.id)
}
// val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume)
// resumeButton.isEnabled = true
// if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null)
// if (activePaused) {
// resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white)
// resumeButton.setOnClickListener {
// resumeButton.isEnabled = false
// onItemClickListener.onResumeClick(item.id)
// }
// }else {
// resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white)
// resumeButton.setOnClickListener {
// resumeButton.isEnabled = false
// onItemClickListener.onPauseClick(item.id)
// }
// }
if (sharedPreferences.getBoolean("paused_downloads", false) || activePaused) {
if (sharedPreferences.getBoolean("paused_downloads", false)) {
progressBar.isIndeterminate = false
cancelButton.isEnabled = true
output.text = activity.getString(R.string.exo_download_paused)
@ -142,7 +150,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
}
interface OnItemClickListener {
fun onCancelClick(itemID: Long)
fun onResumeClick(itemID: Long)
fun onOutputClick(item: DownloadItem)
}

View file

@ -125,7 +125,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
else fileSize.text = fileSizeReadable
val menu = card.findViewById<View>(R.id.options)
val paused = sharedPreferences.getBoolean("paused_downloads", false) || item.status == DownloadRepository.Status.ActivePaused.toString()
//val paused = sharedPreferences.getBoolean("paused_downloads", false) || item.status == DownloadRepository.Status.ActivePaused.toString()
menu.setOnClickListener {
val popup = PopupMenu(activity, it)
popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu)
@ -133,27 +133,27 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
val pause = popup.menu[0]
val resume = popup.menu[1]
if (paused){
pause.isVisible = false
resume.isVisible = true
}else{
pause.isVisible = true
resume.isVisible = false
}
//
// if (paused){
// pause.isVisible = false
// resume.isVisible = true
// }else{
// pause.isVisible = true
// resume.isVisible = false
// }
popup.setOnMenuItemClickListener { m ->
when(m.itemId){
R.id.pause -> {
onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Pause, position)
if (progressBar.progress == 0) progressBar.isIndeterminate = false
popup.dismiss()
}
R.id.resume -> {
onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Resume, position)
progressBar.isIndeterminate = true
popup.dismiss()
}
// R.id.pause -> {
// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Pause, position)
// if (progressBar.progress == 0) progressBar.isIndeterminate = false
// popup.dismiss()
// }
// R.id.resume -> {
// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Resume, position)
// progressBar.isIndeterminate = true
// popup.dismiss()
// }
R.id.cancel -> {
onItemClickListener.onCancelClick(item.id)
popup.dismiss()
@ -166,7 +166,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
}
progressBar.isIndeterminate = !paused
progressBar.isIndeterminate = !sharedPreferences.getBoolean("paused_downloads", false)
card.setOnClickListener {
onItemClickListener.onCardClick()
@ -174,7 +174,6 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
}
interface OnItemClickListener {
fun onCancelClick(itemID: Long)
fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int)
fun onCardClick()
}

View file

@ -88,6 +88,10 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
formatNote.visibility = View.GONE
}
val incognitoLabel = card.findViewById<MaterialButton>(R.id.incognitoLabel)
incognitoLabel.isVisible = item.incognito
val codec = card.findViewById<TextView>(R.id.codec)
val codecText =
if (item.format.encoding != "") {
@ -160,7 +164,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
return oldItem.title == newItem.title &&
oldItem.author == newItem.author &&
oldItem.type == newItem.type &&
oldItem.format == newItem.format
oldItem.format == newItem.format &&
oldItem.incognito == newItem.incognito
}
}
}

View file

@ -96,6 +96,9 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
)
}
val incognitoLabel = card.findViewById<MaterialButton>(R.id.incognitoLabel)
incognitoLabel.isVisible = item.incognito
val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE

View file

@ -0,0 +1,73 @@
package com.deniscerri.ytdl.ui.adapter
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.NavOptionsItemBinding
class NavBarOptionsAdapter(
val items: MutableList<MenuItem>,
var selectedHomeTabId: Int,
onItemClickListener: OnItemClickListener,
) : RecyclerView.Adapter<NavBarOptionsAdapter.NavBarOptionsViewHolder>() {
private val onItemClickListener: OnItemClickListener
init {
this.onItemClickListener = onItemClickListener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NavBarOptionsViewHolder {
val binding = NavOptionsItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return NavBarOptionsViewHolder(binding)
}
override fun getItemCount() = items.size
class NavBarOptionsViewHolder(
val binding: NavOptionsItemBinding
) : RecyclerView.ViewHolder(binding.root)
override fun onBindViewHolder(holder: NavBarOptionsViewHolder, position: Int) {
val item = items[position]
val essential = listOf(R.id.homeFragment, R.id.moreFragment)
val noHome = listOf(R.id.terminalActivity)
holder.binding.apply {
title.text = item.title
checkbox.isChecked = item.isVisible || essential.contains(item.itemId)
checkbox.isEnabled = !essential.contains(item.itemId)
home.setImageResource(
if (item.itemId == selectedHomeTabId)
R.drawable.baseline_home_filled_24 else R.drawable.ic_home_outlined
)
home.isVisible = !noHome.contains(item.itemId)
home.setOnClickListener {
if (!checkbox.isChecked || selectedHomeTabId == item.itemId) {
return@setOnClickListener
}
val oldSelection = items.indexOfFirst { it.itemId == selectedHomeTabId }
selectedHomeTabId = item.itemId
listOf(position, oldSelection).forEach {
notifyItemChanged(it)
}
}
checkbox.setOnClickListener {
item.isVisible = checkbox.isChecked
if (!checkbox.isChecked){
onItemClickListener.onNavBarOptionDeselected(this)
}
}
}
}
interface OnItemClickListener {
fun onNavBarOptionDeselected(item: NavOptionsItemBinding)
}
}

View file

@ -21,11 +21,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -36,21 +34,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private val currentDownloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() {
class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener
private lateinit var sharedPreferences : SharedPreferences
private var incognito: Boolean = currentDownloadItem.incognito
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
onDownloadItemUpdateListener = listener
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -100,7 +96,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
}
//check if the item has formats and its audio-only
val isAudioOnly = result.formats.isNotEmpty() && result.formats.none { !it.format_note.contains("audio") }
val isAudioOnly = currentDownloadItem.allFormats.isNotEmpty() && currentDownloadItem.allFormats.none { !it.format_note.contains("audio") }
if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
@ -111,7 +107,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
fragmentAdapter = DownloadFragmentAdapter(
fragmentManager,
lifecycle,
result,
null,
currentDownloadItem
)
viewPager2.adapter = fragmentAdapter
@ -194,24 +190,39 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
val ok = view.findViewById<Button>(R.id.bottom_sheet_ok)
ok!!.setOnClickListener {
val item = getDownloadItem()
onDownloadItemUpdateListener.onDownloadItemUpdate(result.id, item)
onDownloadItemUpdateListener.onDownloadItemUpdate(item)
dismiss()
}
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
link.text = result.url
link.text = currentDownloadItem.url
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), result.url)
UiUtil.openLinkIntent(requireContext(), currentDownloadItem.url)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), result.url)
UiUtil.copyLinkToClipBoard(requireContext(), currentDownloadItem.url)
true
}
val incognitoBtn = view.findViewById<Button>(R.id.bottomsheet_incognito)
incognitoBtn.alpha = if (incognito) 1f else 0.3f
incognitoBtn.setOnClickListener {
if (incognito) {
it.alpha = 0.3f
}else{
it.alpha = 1f
}
incognito = !incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show()
}
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
return when(selectedTabPosition){
val item = when(selectedTabPosition){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem.apply { id = currentDownloadItem.id }
@ -225,6 +236,8 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
f.downloadItem.apply { id = currentDownloadItem.id }
}
}
item.incognito = incognito
return item
}
@ -261,7 +274,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
interface OnDownloadItemUpdateListener {
fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem)
fun onDownloadItemUpdate(item: DownloadItem)
}
override fun onCancel(dialog: DialogInterface) {

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
@ -17,7 +18,9 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
@ -85,6 +88,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var result: ResultItem
private lateinit var type: Type
private var currentDownloadItem: DownloadItem? = null
private var incognito: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -113,6 +117,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
result = res
currentDownloadItem = dwl
incognito = currentDownloadItem?.incognito ?: sharedPreferences.getBoolean("incognito", false)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -424,6 +429,20 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
(updateItem.parent as LinearLayout).visibility = View.GONE
}
val incognitoBtn = view.findViewById<Button>(R.id.bottomsheet_incognito)
incognitoBtn.alpha = if (incognito) 1f else 0.3f
incognitoBtn.setOnClickListener {
if (incognito) {
it.alpha = 0.3f
}else{
it.alpha = 1f
}
incognito = !incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show()
}
//update in the background if there is no data
if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false) && type != Type.command){
@ -604,7 +623,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem {
return when(selectedTabPosition){
val item = when(selectedTabPosition){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem
@ -618,6 +637,9 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
f.downloadItem
}
}
item.incognito = incognito
return item
}
private fun getAlsoAudioDownloadItem(finished: (it: DownloadItem) -> Unit) {

View file

@ -199,9 +199,10 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
}
freeSpace = view.findViewById(R.id.freespace)
freeSpace.text = String.format(getString(R.string.freespace) + ": " + FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace
))
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
val shortcutCount = withContext(Dispatchers.IO){
commandTemplateViewModel.getTotalShortcutNumber()

View file

@ -10,7 +10,7 @@ import com.deniscerri.ytdl.database.models.ResultItem
class DownloadFragmentAdapter (
fragmentManager : FragmentManager,
lifecycle : Lifecycle,
private var result: ResultItem,
private var result: ResultItem?,
private var downloadItem: DownloadItem?,
private var nonSpecific: Boolean = false
) : FragmentStateAdapter(fragmentManager, lifecycle) {
@ -24,8 +24,8 @@ class DownloadFragmentAdapter (
}
fun setTitleAuthor(t: String, a: String){
result.title = t
result.author = a
result?.title = t
result?.author = a
downloadItem?.title = t
downloadItem?.author = a
}

View file

@ -167,11 +167,11 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
if (items.all { it2 -> it2.type == items[0].type }) {
bottomAppBar.menu[1].icon?.alpha = 255
if (items[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[3].icon?.alpha = 255
bottomAppBar.menu[4].icon?.alpha = 255
}
} else {
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
bottomAppBar.menu[4].icon?.alpha = 30
}
val type = items.first().type
@ -270,8 +270,51 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
lifecycleScope.launch {
val allIncognito = withContext(Dispatchers.IO){
downloadViewModel.areAllProcessingIncognito()
}
bottomAppBar.menu.children.first { it.itemId == R.id.incognito }.icon!!.apply {
alpha = if (allIncognito){
255
}else{
30
}
}
}
bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
R.id.incognito -> {
lifecycleScope.launch {
if (m.icon!!.alpha == 255) {
bottomAppBar.menu[3].isEnabled = false
withContext(Dispatchers.IO) {
downloadViewModel.updateProcessingIncognito(false)
withContext(Dispatchers.Main){
m.icon!!.alpha = 30
m.isEnabled = true
}
}
bottomAppBar.menu[3].icon?.alpha = 30
bottomAppBar.menu[3].isEnabled = true
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.disabled)}", Toast.LENGTH_SHORT).show()
}else{
bottomAppBar.menu[3].isEnabled = false
withContext(Dispatchers.IO) {
downloadViewModel.updateProcessingIncognito(true)
withContext(Dispatchers.Main){
}
}
bottomAppBar.menu[3].icon?.alpha = 255
bottomAppBar.menu[3].isEnabled = true
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.ok)}", Toast.LENGTH_SHORT).show()
}
}
}
R.id.folder -> {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
@ -306,7 +349,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomAppBar.menu[4].icon?.alpha = 255
bottomSheet.cancel()
}
}
@ -318,7 +361,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 255
bottomAppBar.menu[4].icon?.alpha = 255
bottomSheet.cancel()
}
}
@ -330,7 +373,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
bottomAppBar.menu[3].icon?.alpha = 30
bottomAppBar.menu[4].icon?.alpha = 30
bottomSheet.cancel()
}
}
@ -728,11 +771,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.getItemByID(id)
}
val resultItem = withContext(Dispatchers.IO){
resultViewModel.getItemByURL(downloadItem.url)!!
}
if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, this@DownloadMultipleBottomSheetDialog)
val bottomSheet = ConfigureDownloadBottomSheetDialog(downloadItem, this@DownloadMultipleBottomSheetDialog)
bottomSheet.show(parentFragmentManager, "configureDownloadSingleSheet")
}
}
@ -771,7 +811,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onClick(p0: View?) {
}
override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) {
override fun onDownloadItemUpdate(item: DownloadItem) {
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)

View file

@ -125,10 +125,8 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
override fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int) {
val resultItem = downloadViewModel.createResultItemFromDownload(alreadyExistsItem.downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem
) {
val currentIndex = duplicates.indexOf(alreadyExistsItem)
@ -138,7 +136,7 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
adapter.notifyItemChanged(position)
}
}
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, alreadyExistsItem.downloadItem, onItemUpdated)
val bottomSheet = ConfigureDownloadBottomSheetDialog(alreadyExistsItem.downloadItem, onItemUpdated)
bottomSheet.show(requireActivity().supportFragmentManager, "configureDownloadSingleSheet")
}

View file

@ -21,6 +21,7 @@ import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
@ -28,12 +29,14 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.regex.Pattern
class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>? = null, private var _formats: List<List<Format>>? = null, private val _listener: OnFormatClickListener? = null) : BottomSheetDialogFragment() {
@ -66,6 +69,8 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
private lateinit var formats: List<List<Format>>
private lateinit var listener: OnFormatClickListener
private var currentFormatSource : String? = null
enum class FormatSorting {
filesize, container, codec, id
}
@ -157,6 +162,31 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
if (!hasGenericFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) refreshBtn.visibility = View.GONE
val formatSourceLinear = view.findViewById<MaterialCardView>(R.id.formatSourceLinear)
val formatSourceGroup = view.findViewById<RadioGroup>(R.id.format_source_group)
val youtubeURLMatcher = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val canSwitch = items.all { youtubeURLMatcher.matcher(it!!.url).find() }
if (canSwitch) {
val availableSources = resources.getStringArray(R.array.formats_source)
val availableSourcesValues = resources.getStringArray(R.array.formats_source_values)
val currentSource = sharedPreferences.getString("formats_source", "yt-dlp")
formatSourceLinear.isVisible = true
availableSources.forEachIndexed { idx, it ->
val radio = RadioButton(requireContext())
val tag = availableSourcesValues[idx]
radio.text = it
radio.tag = tag
radio.setOnClickListener { compoundButton ->
formatSourceGroup.clearCheck()
formatSourceGroup.check(compoundButton.id)
currentFormatSource = compoundButton.tag.toString()
refreshBtn.performClick()
}
formatSourceGroup.addView(radio)
}
formatSourceGroup.check(formatSourceGroup.children.firstOrNull { it.tag == currentSource }!!.id)
}
refreshBtn.setOnClickListener {
lifecycleScope.launch {
@ -172,6 +202,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
chosenFormats = emptyList()
refreshBtn.isEnabled = false
formatSourceLinear.isVisible = false
formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE
shimmers.startShimmer()
@ -182,7 +213,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
formatCollection.clear()
kotlin.runCatching {
val res = withContext(Dispatchers.IO){
infoUtil.getFormats(items.first()!!.url)
infoUtil.getFormats(items.first()!!.url, currentFormatSource)
}
res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) {
@ -215,7 +246,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
refreshBtn.text = progress
}
withContext(Dispatchers.IO){
infoUtil.getFormatsMultiple(items.map { it!!.url }) {
infoUtil.getFormatsMultiple(items.map { it!!.url }, currentFormatSource) {
if (isActive) {
lifecycleScope.launch(Dispatchers.Main) {
formatCollection.add(it)
@ -259,6 +290,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
withContext(Dispatchers.Main){
shimmers.visibility = View.GONE
shimmers.stopShimmer()
formatSourceLinear.isVisible = canSwitch
filterBtn.isVisible = !hasGenericFormats
addFormatsToView()
refreshBtn.isVisible = hasGenericFormats
@ -271,6 +303,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
refreshBtn.text = getString(R.string.update_formats)
formatListLinearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE
formatSourceLinear.isVisible = canSwitch
shimmers.stopShimmer()
e.printStackTrace()
@ -393,23 +426,16 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
FormatSorting.filesize -> chosenFormats
}
val formatSorter = FormatSorter(requireContext())
//filter category
when(filterBy){
FormatCategory.ALL -> {}
FormatCategory.SUGGESTED -> {
finalFormats = if (items.first()?.type == Type.audio){
val req = downloadViewModel.getPreferredAudioRequirements()
finalFormats
.filter { f -> req.sumOf{req -> req(f)} >= 1 }
.sortedByDescending { f -> req.sumOf{req -> req(f)} }
.ifEmpty { listOf(downloadViewModel.getFormat(finalFormats, Type.audio)) }
formatSorter.sortAudioFormats(finalFormats)
}else{
val req = downloadViewModel.getPreferredVideoRequirements()
req.addAll(downloadViewModel.getPreferredAudioRequirements())
finalFormats
.filter { f -> req.sumOf { req -> req(f) } >= 1 }
.sortedByDescending { f -> req.sumOf { req -> req(f) } }
.ifEmpty { listOf(downloadViewModel.getFormat(finalFormats, Type.video)) }
formatSorter.sortVideoFormats(finalFormats)
}
}
FormatCategory.SMALLEST -> {

View file

@ -40,6 +40,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.TextWithSubtitle
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -98,6 +99,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var endsAfterNr: TextInputLayout
private lateinit var retryMissingDownloads: MaterialSwitch
private lateinit var getOnlyNewUploads: MaterialSwitch
private lateinit var syncWithSource: MaterialSwitch
private lateinit var resetProcessedLinks: MaterialSwitch
@ -262,9 +264,14 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfterNr = view.findViewById(R.id.after_nr)
retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads)
getOnlyNewUploads = view.findViewById(R.id.get_new_uploads)
syncWithSource = view.findViewById(R.id.sync_with_source)
resetProcessedLinks = view.findViewById(R.id.reset_processed_links)
okButton = view.findViewById(R.id.okButton)
// getOnlyNewUploads.text = TextWithSubtitle(getString(R.string.get_new_uploads), getString(R.string.get_new_uploads))
// syncWithSource.text = TextWithSubtitle(getString(R.string.sync_with_source), getString(R.string.sync_with_source))
// resetProcessedLinks.text = TextWithSubtitle(getString(R.string.reset_processed_links), getString(R.string.reset_processed_links))
title.editText!!.setText(currentItem?.name ?: "")
title.editText?.doAfterTextChanged { checkIfValid() }
@ -469,6 +476,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
retryMissingDownloads.isChecked = currentItem?.retryMissingDownloads ?: false
getOnlyNewUploads.isChecked = currentItem?.getOnlyNewUploads ?: false
syncWithSource.isChecked = currentItem?.syncWithSource ?: false
resetProcessedLinks.isVisible = currentItem != null
if (currentItem != null) okButton.text = getString(R.string.update)
@ -518,6 +526,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfterCount = endsAfterCount,
runCount = 0,
getOnlyNewUploads = getOnlyNewUploads.isChecked,
syncWithSource = syncWithSource.isChecked,
retryMissingDownloads = retryMissingDownloads.isChecked,
alreadyProcessedLinks = if (resetProcessedLinks.isChecked){
mutableListOf()

View file

@ -597,47 +597,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
cancelActiveDownload(itemID)
}
override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
when(action){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
lifecycleScope.launch {
cancelItem(itemID.toInt())
item.status = DownloadRepository.Status.ActivePaused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeAdapter.notifyItemChanged(position)
}
}
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch {
item.status = DownloadRepository.Status.Queued.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
activeAdapter.notifyItemChanged(position)
val queue = if (activeCount > 1) listOf(item)
else withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list.add(0, item)
list
}
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
}
}
}
override fun onCardClick() {
this.dismiss()
findNavController().navigate(
@ -645,12 +604,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
)
}
private fun cancelItem(id: Int){
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id)
}
private fun cancelActiveDownload(itemID: Long){
lifecycleScope.launch {
val id = itemID.toInt()

View file

@ -1,14 +1,27 @@
package com.deniscerri.ytdl.ui.downloads
import android.animation.AnimatorSet
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.content.SharedPreferences
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -16,21 +29,36 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
@ -42,14 +70,27 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var activeDownloadsLinear: LinearLayout
private lateinit var activeDownloadsNoResults: View
private lateinit var activeRecyclerView : RecyclerView
private lateinit var activeDownloads : ActiveDownloadAdapter
private lateinit var queuedDownloadsLinear: LinearLayout
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : QueuedDownloadAdapter
private lateinit var queuedFilesizeCount : TextView
private var queuedTotalSize = 0
private lateinit var queuedDragHandleToggle : TextView
private var queuedActionMode : ActionMode? = null
private lateinit var inQueueLabel : MaterialButton
private lateinit var activeQueuedLinear: LinearLayout
private lateinit var notificationUtil: NotificationUtil
private lateinit var pause: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton
@ -67,7 +108,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
return fragmentView
}
@SuppressLint("NotifyDataSetChanged")
@OptIn(ExperimentalBadgeUtils::class)
@SuppressLint("NotifyDataSetChanged", "RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -77,12 +119,34 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager = WorkManager.getInstance(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
activeDownloadsLinear = view.findViewById(R.id.active_linear)
activeDownloadsNoResults = view.findViewById(R.id.active_no_results)
activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview)
activeRecyclerView.forceFastScrollMode()
activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
queuedDownloadsLinear = view.findViewById(R.id.queue_linear)
val itemTouchHelper = ItemTouchHelper(queuedDragDropHelper)
queuedDownloads = QueuedDownloadAdapter(this, requireActivity(), itemTouchHelper)
queuedRecyclerView = view.findViewById(R.id.queued_recyclerview)
queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = queuedDownloads
queuedRecyclerView.enableFastScroll()
itemTouchHelper.attachToRecyclerView(queuedRecyclerView)
queuedFilesizeCount = view.findViewById(R.id.filesize)
queuedDragHandleToggle = view.findViewById(R.id.drag)
inQueueLabel = view.findViewById(R.id.in_queue_label)
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("queued")){
val swipeItemTouchHelper = ItemTouchHelper(queuedSwipeCallback)
swipeItemTouchHelper.attachToRecyclerView(queuedRecyclerView)
}
queuedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
activeQueuedLinear = view.findViewById(R.id.active_queued_linear)
pause = view.findViewById(R.id.pause)
pause.isVisible = false
resume = view.findViewById(R.id.resume)
@ -91,56 +155,123 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pause.setOnClickListener {
lifecycleScope.launch {
workManager.cancelAllWorkByTag("download")
withContext(Dispatchers.IO){
val activeDownloadsList = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.forEach {
}
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
preferences.edit().putBoolean("paused_downloads", true).apply()
pause.isEnabled = false
activeDownloads.notifyDataSetChanged()
resume.isVisible = true
pause.isVisible = false
withContext(Dispatchers.Main){
delay(1000)
pause.isVisible = false
pause.isEnabled = true
resume.isVisible = true
}
}
}
resume.setOnClickListener {
lifecycleScope.launch {
preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false
pause.isVisible = true
resume.isEnabled = false
withContext(Dispatchers.IO) {
downloadViewModel.resetActivePaused()
downloadViewModel.resetActiveToQueued()
downloadViewModel.startDownloadWorker(listOf())
withContext(Dispatchers.Main){
activeDownloads.notifyDataSetChanged()
}
}
withContext(Dispatchers.Main){
delay(1000)
resume.isVisible = false
resume.isEnabled = true
pause.isVisible = true
}
}
}
lifecycleScope.launch {
val activeQueuedDownloadsCount = withContext(Dispatchers.IO) {
downloadViewModel.getActiveDownloadsCount()
}
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = activeQueuedDownloadsCount > 0 && !pausedDownloads
resume.isVisible = activeQueuedDownloadsCount > 0 && pausedDownloads
}
lifecycleScope.launch {
downloadViewModel.activeQueuedDownloadsCount.collectLatest {
activeQueuedLinear.isVisible = it > 0
noResults.isVisible = it == 0
if (it == 0) {
pause.isVisible = false
resume.isVisible = false
}
}
}
lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest {
activeDownloadsNoResults.isVisible = it.isEmpty()
activeDownloads.submitList(it)
}
}
lifecycleScope.launch {
downloadViewModel.queuedDownloads.collectLatest {
queuedDownloads.submitData(it)
}
}
queuedDownloads.addLoadStateListener { loadState ->
lifecycleScope.launch {
if (loadState.append.endOfPaginationReached )
{
if (queuedDownloads.itemCount < 1){
queuedFilesizeCount.visibility = View.GONE
}else{
val size = withContext(Dispatchers.IO){
FileUtil.convertFileSize(downloadViewModel.getQueuedCollectedFileSize())
}
if (size == "?") queuedFilesizeCount.visibility = View.GONE
else {
queuedFilesizeCount.visibility = View.VISIBLE
queuedFilesizeCount.text = "${getString(R.string.file_size)}: ~ $size"
}
}
}
}
}
lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest {
noResults.isVisible = it == 0
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = it > 0 && !pausedDownloads
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued)).observe(viewLifecycleOwner){
queuedDownloadsLinear.isVisible = it > 0
queuedTotalSize = it
queuedDragHandleToggle.isVisible = it > 1
if (it > 0) {
val badgeDrawable = BadgeDrawable.create(requireContext())
badgeDrawable.number = it
badgeDrawable.isVisible = true
badgeDrawable.verticalOffset = 25
requireView().post {
BadgeUtils.attachBadgeDrawable(badgeDrawable, inQueueLabel)
}
}
}
}
lifecycleScope.launch {
downloadViewModel.activeQueuedDownloadsCount.collectLatest {
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
resume.isVisible = it > 0 && pausedDownloads
}
}
lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest {
delay(100)
activeDownloads.submitList(it)
}
queuedDragHandleToggle.setOnClickListener {
queuedDownloads.toggleShowDragHandle()
}
}
//dont remove
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
@ -154,6 +285,10 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
fun hidePauseBtn(){
pause.isVisible = false
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
@ -205,17 +340,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
override fun onResumeClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(listOf(item))
}
}
}
override fun onOutputClick(item: DownloadItem) {
if (item.logID != null && item.logID != 0L) {
val bundle = Bundle()
@ -227,4 +351,398 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
//QUEUED ITEMS FUNCTIONS
private val queuedContextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
mode.title = "${queuedDownloads.getSelectedObjectsCount(queuedTotalSize)} ${getString(R.string.selected)}"
return true
}
override fun onPrepareActionMode(
mode: ActionMode?,
menu: Menu?
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode?,
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.select_between -> {
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(
DownloadRepository.Status.Queued).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){
queuedDownloads.checkMultipleItems(idsInMiddle)
queuedActionMode?.title = "${idsInMiddle.count()} ${getString(R.string.selected)}"
}
mode?.menu?.findItem(R.id.select_between)?.isVisible = false
}
true
}
R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.deleteAllWithID(selectedObjects)
queuedActionMode?.finish()
}
}
deleteDialog.show()
true
}
R.id.select_all -> {
queuedDownloads.checkAll()
mode?.title = "(${queuedDownloads.getSelectedObjectsCount(queuedTotalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {
queuedDownloads.invertSelected()
val selectedObjects = queuedDownloads.getSelectedObjectsCount(queuedTotalSize)
queuedActionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0) queuedActionMode?.finish()
true
}
R.id.up -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtTopOfQueue(selectedObjects)
}
queuedActionMode?.finish()
}
true
}
R.id.down -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtBottomOfQueue(selectedObjects)
}
queuedActionMode?.finish()
}
true
}
R.id.copy_urls -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
val urls = withContext(Dispatchers.IO){
downloadViewModel.getURLsByIds(selectedObjects)
}
UiUtil.copyToClipboard(urls.joinToString("\n"), requireActivity())
queuedActionMode?.finish()
}
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
queuedActionMode = null
queuedDownloads.clearCheckedItems()
}
suspend fun getSelectedIDs() : List<Long>{
return if (queuedDownloads.inverted || queuedDownloads.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(queuedDownloads.checkedItems.toList(), listOf(
DownloadRepository.Status.Queued))
}
}else{
queuedDownloads.checkedItems.toList()
}
}
}
var movedToNewPositionID = 0L
private val queuedDragDropHelper: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
movedToNewPositionID = target.itemView.tag.toString().toLong()
queuedDownloads.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (ItemTouchHelper.ACTION_STATE_DRAG == actionState) {
/**
* Change alpha, scale and elevation on drag.
*/
(viewHolder?.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 0.7f),
UiUtil.getScaleXAnimator(it, 1.02f),
UiUtil.getScaleYAnimator(it, 1.02f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_6dp)
)
}.start()
}
}
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
/**
* Clear alpha, scale and elevation after drag/swipe
*/
(viewHolder.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 1f),
UiUtil.getScaleXAnimator(it, 1f),
UiUtil.getScaleYAnimator(it, 1f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_2dp)
)
}.start()
}
downloadViewModel.putAtPosition(viewHolder.itemView.tag.toString().toLong(), movedToNewPositionID)
}
override fun isLongPressDragEnabled(): Boolean {
return false
}
}
private var queuedSwipeCallback: 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 itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
queuedDownloads.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeQueuedItem(deletedItem.id)
}
}
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
RecyclerViewSwipeDecorator.Builder(
requireContext(),
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
.addSwipeLeftBackgroundColor(Color.RED)
.addSwipeLeftActionIcon(R.drawable.baseline_delete_24)
.addSwipeRightBackgroundColor(
MaterialColors.getColor(
requireContext(),
R.attr.colorOnSurfaceInverse, Color.TRANSPARENT
)
)
.create()
.decorate()
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
override fun onQueuedCancelClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toInt().toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
}
}
}
override fun onMoveQueuedItemToTop(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtTopOfQueue(listOf(itemID))
}
}
override fun onMoveQueuedItemToBottom(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtBottomOfQueue(listOf(itemID))
}
}
override fun onQueuedCardClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeQueuedItem(it.id)
},
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
lifecycleScope.launch {
it.status = DownloadRepository.Status.Saved.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(it.id, DownloadRepository.Status.Saved)
}
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
)
)
}
},
scheduleButtonClick = {downloadItem ->
UiUtil.showDatePicker(parentFragmentManager) {
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
}
}
)
}
}
override fun onQueuedCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = queuedDownloads.getSelectedObjectsCount(queuedTotalSize)
if (queuedActionMode == null) queuedActionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(queuedContextualActionBar)
val now = System.currentTimeMillis()
queuedActionMode?.apply {
if (selectedObjects == 0){
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.down).isVisible = position < queuedTotalSize
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = queuedContextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued).toListString())
}
this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty()
}
}
}
}
}
private fun removeQueuedItem(id: Long){
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
}
deleteDialog.show()
}
}
}

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
@ -16,6 +17,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -29,7 +31,9 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.downloadcard.DownloadAudioFragment
import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar
@ -52,6 +56,7 @@ class DownloadQueueMainFragment : Fragment(){
private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadListFragmentAdapter
private lateinit var mainActivity: MainActivity
private lateinit var notificationUtil: NotificationUtil
private lateinit var sharedPreferences: SharedPreferences
override fun onCreateView(
@ -60,6 +65,7 @@ class DownloadQueueMainFragment : Fragment(){
savedInstanceState: Bundle?
): View? {
mainActivity = activity as MainActivity
notificationUtil = NotificationUtil(mainActivity)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
return inflater.inflate(R.layout.fragment_download_queue_main_screen, container, false)
}
@ -69,7 +75,11 @@ class DownloadQueueMainFragment : Fragment(){
workManager = WorkManager.getInstance(requireContext())
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
topAppBar = view.findViewById(R.id.logs_toolbar)
topAppBar = view.findViewById(R.id.downloads_toolbar)
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
if (isInNavBar) {
topAppBar.navigationIcon = null
}
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
tabLayout = view.findViewById(R.id.download_tablayout)
@ -80,7 +90,7 @@ class DownloadQueueMainFragment : Fragment(){
overScrollMode = View.OVER_SCROLL_NEVER
}
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
val fragments = mutableListOf(ActiveDownloadsFragment(), /*QueuedDownloadsFragment(),*/ ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
fragmentAdapter = DownloadListFragmentAdapter(
childFragmentManager,
@ -94,11 +104,11 @@ class DownloadQueueMainFragment : Fragment(){
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
when (position) {
0 -> tab.text = getString(R.string.running)
1 -> tab.text = getString(R.string.in_queue)
2 -> tab.text = getString(R.string.scheduled)
3 -> tab.text = getString(R.string.cancelled)
4 -> tab.text = getString(R.string.errored)
5 -> tab.text = getString(R.string.saved)
// 1 -> tab.text = getString(R.string.in_queue)
1 -> tab.text = getString(R.string.scheduled)
2 -> tab.text = getString(R.string.cancelled)
3 -> tab.text = getString(R.string.errored)
4 -> tab.text = getString(R.string.saved)
}
}.attach()
@ -124,11 +134,12 @@ class DownloadQueueMainFragment : Fragment(){
initMenu()
if (arguments?.getString("tab") != null){
tabLayout.getTabAt(4)!!.select()
tabLayout.getTabAt(3)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(4, false)
viewPager2.setCurrentItem(3, false)
val reconfigureID = arguments?.getLong("reconfigure")
reconfigureID?.apply {
notificationUtil.cancelErroredNotification(this.toInt())
lifecycleScope.launch {
kotlin.runCatching {
val item = withContext(Dispatchers.IO){
@ -155,30 +166,30 @@ class DownloadQueueMainFragment : Fragment(){
}
}
}
// lifecycleScope.launch {
// downloadViewModel.queuedDownloadsCount.collectLatest {
// tabLayout.getTabAt(1)?.apply {
// createBadge(it)
// }
// }
// }
lifecycleScope.launch {
downloadViewModel.queuedDownloadsCount.collectLatest {
downloadViewModel.scheduledDownloadsCount.collectLatest {
tabLayout.getTabAt(1)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.scheduledDownloadsCount.collectLatest {
downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(2)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(3)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply {
tabLayout.getTabAt(3)?.apply {
removeBadge()
if (it > 0) createBadge(it)
}
@ -186,7 +197,7 @@ class DownloadQueueMainFragment : Fragment(){
}
lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest {
tabLayout.getTabAt(5)?.apply {
tabLayout.getTabAt(4)?.apply {
removeBadge()
if (it > 0) createBadge(it)
}
@ -264,7 +275,10 @@ class DownloadQueueMainFragment : Fragment(){
deleteDialog.show()
}
@SuppressLint("NotifyDataSetChanged")
private fun cancelAllDownloads() {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
fragmentAdapter.notifyDataSetChanged()
workManager.cancelAllWorkByTag("download")
lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext())
@ -278,4 +292,9 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.cancelActiveQueued()
}
}
fun scrollToActive(){
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, true)
}
}

View file

@ -48,6 +48,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.ui.adapter.HistoryAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
@ -200,8 +201,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
initChips()
}
private fun scrollToTop() {
recyclerView!!.scrollToPosition(0)
fun scrollToTop() {
recyclerView.scrollToPosition(0)
Handler(Looper.getMainLooper()).post {
(topAppBar!!.parent as AppBarLayout).setExpanded(
true,
@ -239,6 +240,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
}
})
topAppBar!!.setOnClickListener { scrollToTop() }
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
topAppBar!!.menu.findItem(R.id.download_queue).isVisible = !showingDownloadQueue
topAppBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
R.id.remove_history -> {
@ -402,7 +405,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
val websites = mutableListOf<String>()
val websiteFilter = historyViewModel.websiteFilter.value
for (item in list){
if (!websites.contains(item.website.lowercase())) websites.add(item.website.lowercase())
if (!websites.contains(item.website)) websites.add(item.website)
}
websiteGroup!!.removeAllViews()
if (websites.size <= 1) {

View file

@ -112,11 +112,8 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
override fun onActionButtonClick(itemID: Long) {
lifecycleScope.launch {
runCatching {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(listOf(item.id))
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(listOf(itemID))
}
}.onFailure {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_LONG).show()

View file

@ -9,6 +9,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -20,6 +21,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdl.util.NavbarUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -63,6 +65,9 @@ class MoreFragment : Fragment() {
terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings)
val showingTerminal = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.terminalActivity && n.isVisible }
terminal.isVisible = !showingTerminal
terminal.setOnClickListener {
val intent = Intent(context, TerminalActivity::class.java)
startActivity(intent)
@ -76,6 +81,9 @@ class MoreFragment : Fragment() {
findNavController().navigate(R.id.commandTemplatesFragment)
}
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
downloadQueue.isVisible = !showingDownloadQueue
downloadQueue.setOnClickListener {
findNavController().navigate(R.id.downloadQueueMainFragment)
}

View file

@ -2,26 +2,42 @@ package com.deniscerri.ytdl.ui.more.settings
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.LocaleListCompat
import androidx.core.view.forEach
import androidx.navigation.fragment.findNavController
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.NavOptionsItemBinding
import com.deniscerri.ytdl.ui.adapter.NavBarOptionsAdapter
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.Locale
@ -36,13 +52,17 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
private var showTerminalShareIcon: SwitchPreferenceCompat? = null
private var ignoreBatteryOptimization: Preference? = null
private var displayOverApps: SwitchPreferenceCompat? = null
private lateinit var preferences: SharedPreferences
private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.general_preferences, rootKey)
NavbarUtil.init(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
updateUtil = UpdateUtil(requireContext())
val editor = preferences.edit()
WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){
activeDownloadCount = 0
@ -66,6 +86,90 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
true
}
findPreference<Preference>("label_visibility")?.apply {
isVisible = !resources.getBoolean(R.bool.uses_side_nav)
setOnPreferenceChangeListener { preference, newValue ->
restartApp()
true
}
}
findPreference<Preference>("navigation_bar")?.apply {
isVisible = !resources.getBoolean(R.bool.uses_side_nav)
setOnPreferenceClickListener {
val binding = requireActivity().layoutInflater.inflate(R.layout.simple_options_recycler, null)
val options = NavbarUtil.getNavBarItems(requireContext())
val optionsRecycler = binding.findViewById<RecyclerView>(R.id.options_recycler)
var adapter : NavBarOptionsAdapter? = null;
val onitemClick = object: NavBarOptionsAdapter.OnItemClickListener {
override fun onNavBarOptionDeselected(item: NavOptionsItemBinding) {
optionsRecycler.findViewHolderForLayoutPosition(0)?.apply {
(this as NavBarOptionsAdapter.NavBarOptionsViewHolder).apply {
this.binding.home.performClick()
}
}
}
}
adapter = NavBarOptionsAdapter(
options.toMutableList(),
NavbarUtil.getStartFragmentId(requireContext()),
onitemClick
)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
adapter.items.remove(itemToMove)
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
adapter.notifyItemMoved(
viewHolder.absoluteAdapterPosition,
target.absoluteAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}
}
optionsRecycler.layoutManager = LinearLayoutManager(context)
optionsRecycler.adapter = adapter
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(optionsRecycler)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.navigation_bar)
.setView(binding)
.setPositiveButton(R.string.ok) { _, _ ->
NavbarUtil.setNavBarItems(adapter.items, requireContext())
NavbarUtil.setStartFragment(adapter.selectedHomeTabId)
restartApp()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
}
theme!!.summary = theme!!.entry
theme!!.onPreferenceChangeListener =
@ -153,6 +257,14 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
startActivity(intent)
true
}
findPreference<Preference>("piped_instance")?.setOnPreferenceClickListener {
UiUtil.showPipedInstancesDialog(requireActivity(), preferences.getString("piped_instance", "")!!){
editor.putString("piped_instance", it)
editor.apply()
}
true
}
}
override fun onResume() {
@ -164,6 +276,15 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
super.onResume()
}
private fun restartApp() {
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
requireActivity().finishAffinity()
activity?.finishAffinity()
}
private var displayOverAppsResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
@ -171,5 +292,4 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
}
}

View file

@ -1,17 +1,24 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.util.LayoutDirection
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
@ -24,6 +31,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
@ -31,6 +39,7 @@ import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.models.CookieItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.RestoreAppDataItem
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.models.SearchHistoryItem
import com.deniscerri.ytdl.database.models.TemplateShortcut
@ -42,13 +51,17 @@ import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -418,11 +431,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
lifecycleScope.launch {
val preferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
runCatching {
val ip = requireContext().contentResolver.openInputStream(result.data!!.data!!)
val r = BufferedReader(InputStreamReader(ip))
@ -432,308 +441,148 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
total.append(line).append('\n')
}
//PARSE RESTORE JSON
val json = Gson().fromJson(total.toString(), JsonObject::class.java)
val finalMessage = StringBuilder("")
val errorMessage = StringBuilder("")
val restoreData = RestoreAppDataItem()
val parsedDataMessage = StringBuilder()
//preference restore
kotlin.runCatching {
if(json.has("settings")){
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
val prefs = json.getAsJsonArray("settings")
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
finalMessage.append("${getString(R.string.settings)}: ${prefs.count()}\n")
}
}
}.onFailure {
errorMessage.append("\n PREFERENCE RESTORE FAILED: \n ${it.message}")
if (json.has("settings")) {
restoreData.settings = json.getAsJsonArray("settings")
parsedDataMessage.appendLine("${getString(R.string.settings)}: ${restoreData.settings!!.size()}")
}
//history restore
kotlin.runCatching {
if(json.has("downloads")){
val items = json.getAsJsonArray("downloads")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
historyViewModel.insert(item)
}
}
finalMessage.append("${getString(R.string.downloads)}: ${items.count()}\n")
if (json.has("downloads")) {
restoreData.downloads = json.getAsJsonArray("downloads").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n HISTORY RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.downloads)}: ${restoreData.downloads!!.size}")
}
//queued downloads restore
kotlin.runCatching {
if(json.has("queued")){
val items = json.getAsJsonArray("queued")
val queued = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
queued.add(item)
}
if (queued.isNotEmpty()) {
finalMessage.append("${getString(R.string.in_queue)}: ${queued.count()}\n")
withContext(Dispatchers.IO){downloadViewModel.queueDownloads(queued)}
}
if (json.has("queued")) {
restoreData.queued = json.getAsJsonArray("queued").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n QUEUED DOWNLOADS RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.queue)}: ${restoreData.queued!!.size}")
}
//scheduled downloads restore
kotlin.runCatching {
if(json.has("scheduled")){
val items = json.getAsJsonArray("scheduled")
val scheduled = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
scheduled.add(item)
}
if (scheduled.isNotEmpty()) {
finalMessage.append("${getString(R.string.scheduled)}: ${scheduled.count()}\n")
withContext(Dispatchers.IO){downloadViewModel.queueDownloads(scheduled)}
}
if (json.has("scheduled")) {
restoreData.scheduled = json.getAsJsonArray("scheduled").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n SCHEDULED DOWNLOADS RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.scheduled)}: ${restoreData.scheduled!!.size}")
}
//cancelled downloads restore
kotlin.runCatching {
if(json.has("cancelled")){
val items = json.getAsJsonArray("cancelled")
val cancelled = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
cancelled.add(item)
}
cancelled.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(cancelled.isNotEmpty()){
finalMessage.append("${getString(R.string.cancelled)}: ${cancelled.count()}\n")
}
if (json.has("cancelled")) {
restoreData.cancelled = json.getAsJsonArray("cancelled").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n CANCELLED DOWNLOADS RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.cancelled)}: ${restoreData.cancelled!!.size}")
}
//erorred downloads restore
kotlin.runCatching {
if(json.has("errored")){
val items = json.getAsJsonArray("errored")
val errored = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
errored.add(item)
}
errored.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(errored.isNotEmpty()){
finalMessage.append("${getString(R.string.errored)}: ${errored.count()}\n")
}
if (json.has("errored")) {
restoreData.errored = json.getAsJsonArray("errored").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n ERRORED DOWNLOADS RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.errored)}: ${restoreData.errored!!.size}")
}
//saved downloads restore
kotlin.runCatching {
if(json.has("saved")){
val items = json.getAsJsonArray("saved")
val saved = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
saved.add(item)
}
saved.asReversed().forEach { f ->
withContext(Dispatchers.IO){
downloadViewModel.insert(f)
}
}
if(saved.isNotEmpty()){
finalMessage.append("${getString(R.string.saved)}: ${saved.count()}\n")
}
if (json.has("saved")) {
restoreData.saved = json.getAsJsonArray("saved").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n SAVED DOWNLOADS RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.saved)}: ${restoreData.saved!!.size}")
}
//cookies restore
kotlin.runCatching {
if(json.has("cookies")){
val items = json.getAsJsonArray("cookies")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
cookieViewModel.insert(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.cookies)}: ${items.count()}\n")
}
if (json.has("cookies")) {
restoreData.cookies = json.getAsJsonArray("cookies").map {
val item =
Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n COOKIES RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.cookies)}: ${restoreData.cookies!!.size}")
}
//command template restore
kotlin.runCatching {
if(json.has("templates")){
val items = json.getAsJsonArray("templates")
val templates = mutableListOf<CommandTemplate>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java)
item.id = 0L
templates.add(item)
}
templates.asReversed().forEach { t ->
withContext(Dispatchers.IO){
commandTemplateViewModel.insert(t)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.command_templates)}: ${items.count()}\n")
}
if (json.has("templates")) {
restoreData.templates = json.getAsJsonArray("templates").map {
val item = Gson().fromJson(
it.toString().replace("^\"|\"$", ""),
CommandTemplate::class.java
)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n COMMAND TEMPLATE RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.command_templates)}: ${restoreData.templates!!.size}")
}
//shortcuts restore
kotlin.runCatching {
if(json.has("shortcuts")){
val items = json.getAsJsonArray("shortcuts")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), TemplateShortcut::class.java)
item.id = 0L
withContext(Dispatchers.IO){
commandTemplateViewModel.insertShortcut(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.shortcuts)}: ${items.count()}\n")
}
if (json.has("shortcuts")) {
restoreData.shortcuts = json.getAsJsonArray("shortcuts").map {
val item = Gson().fromJson(
it.toString().replace("^\"|\"$", ""),
TemplateShortcut::class.java
)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n COMMAND SHORTCUTS RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.shortcuts)}: ${restoreData.shortcuts!!.size}")
}
//search history restore
kotlin.runCatching {
if(json.has("search_history")){
val items = json.getAsJsonArray("search_history")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), SearchHistoryItem::class.java)
withContext(Dispatchers.IO){
resultViewModel.addSearchQueryToHistory(item.query)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.search_history)}: ${items.count()}\n")
}
if (json.has("search_history")) {
restoreData.searchHistory = json.getAsJsonArray("search_history").map {
val item = Gson().fromJson(
it.toString().replace("^\"|\"$", ""),
SearchHistoryItem::class.java
)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n SEARCH HISTORY RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.search_history)}: ${restoreData.searchHistory!!.size}")
}
//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)
}
}
if (json.has("observe_sources")) {
restoreData.observeSources = json.getAsJsonArray("observe_sources").map {
val item = Gson().fromJson(
it.toString().replace("^\"|\"$", ""),
ObserveSourcesItem::class.java
)
item.id = 0L
item
}
}.onFailure {
errorMessage.append("\n SEARCH HISTORY RESTORE FAILED: \n ${it.message}")
parsedDataMessage.appendLine("${getString(R.string.observe_sources)}: ${restoreData.observeSources!!.size}")
}
if (finalMessage.isEmpty()) throw Exception("")
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.restore))
builder.setMessage("${getString(R.string.restore_complete)}\n $finalMessage \n $errorMessage")
builder.setPositiveButton(
getString(R.string.restart)
) { _: DialogInterface?, _: Int ->
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
requireActivity().finishAffinity()
if(json.has("settings")){
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(preferences.getString("app_language", "en")))
showAppRestoreInfoDialog(
onMerge = {
restoreData(restoreData,parsedDataMessage.toString())
},
onReset = {
restoreData(restoreData,parsedDataMessage.toString(), true)
}
activity?.finishAffinity()
}
)
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int ->
if(json.has("settings")){
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(preferences.getString("app_language", "en")))
}
}
val dialog = builder.create()
dialog.show()
}.onFailure {
it.printStackTrace()
Snackbar.make(requireView(), getString(R.string.couldnt_parse_file), Snackbar.LENGTH_LONG).show()
@ -743,4 +592,185 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
}
}
@SuppressLint("RestrictedApi")
private fun showAppRestoreInfoDialog(onMerge: () -> Unit, onReset: () -> Unit){
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.restore))
builder.setMessage(getString(R.string.restore_info))
builder.setNegativeButton(getString(R.string.cancel)) { dialog : DialogInterface?, _: Int ->
dialog?.dismiss()
}
builder.setPositiveButton(getString(R.string.restore)) { dialog : DialogInterface?, _: Int ->
onMerge()
dialog?.dismiss()
}
builder.setNeutralButton(getString(R.string.reset)) { dialog : DialogInterface?, _: Int ->
onReset()
dialog?.dismiss()
}
val dialog = builder.create()
dialog.show()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
private fun restoreData(data: RestoreAppDataItem, restoreDataMessage: String, resetData: Boolean = false) = lifecycleScope.launch {
data.settings?.apply {
val prefs = this
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
}
}
data.downloads?.apply {
withContext(Dispatchers.IO){
if (resetData) historyViewModel.deleteAll(false)
data.downloads!!.forEach {
historyViewModel.insert(it)
}
}
}
data.queued?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteQueued()
data.queued!!.forEach {
downloadViewModel.insert(it)
}
downloadViewModel.queueDownloads(listOf())
}
}
data.cancelled?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteCancelled()
data.cancelled!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.errored?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteErrored()
data.errored!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.saved?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteSaved()
data.saved!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.cookies?.apply {
withContext(Dispatchers.IO){
if (resetData) cookieViewModel.deleteAll()
data.cookies!!.forEach {
cookieViewModel.insert(it)
}
}
}
data.templates?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateViewModel.deleteAll()
data.templates!!.forEach {
commandTemplateViewModel.insert(it)
}
}
}
data.shortcuts?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateViewModel.deleteAllShortcuts()
data.shortcuts!!.forEach {
commandTemplateViewModel.insertShortcut(it)
}
}
}
data.searchHistory?.apply {
withContext(Dispatchers.IO){
if (resetData) resultViewModel.deleteAllSearchQueryHistory()
data.searchHistory!!.forEach {
resultViewModel.addSearchQueryToHistory(it.query)
}
}
}
data.observeSources?.apply {
withContext(Dispatchers.IO){
if (resetData) observeSourcesViewModel.deleteAll()
data.observeSources!!.forEach {
observeSourcesViewModel.insert(it)
}
}
}
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.restore))
builder.setMessage("${getString(R.string.restore_complete)}\n $restoreDataMessage")
builder.setPositiveButton(
getString(R.string.restart)
) { _: DialogInterface?, _: Int ->
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
requireActivity().finishAffinity()
if(data.settings != null){
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(preferences.getString("app_language", "en")))
}
activity?.finishAffinity()
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int ->
if(data.settings != null){
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(preferences.getString("app_language", "en")))
}
}
val dialog = builder.create()
dialog.show()
}
}

View file

@ -55,13 +55,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true
}
findPreference<Preference>("piped_instance")?.setOnPreferenceClickListener {
UiUtil.showPipedInstancesDialog(requireActivity(), preferences.getString("piped_instance", "")!!){
editor.putString("piped_instance", it)
editor.apply()
}
true
}
findPreference<Preference>("changelog")?.setOnPreferenceClickListener {
lifecycleScope.launch(Dispatchers.IO) {

View file

@ -17,6 +17,8 @@ import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import android.text.Html
import android.text.Spanned
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.MotionEvent
@ -30,6 +32,7 @@ import android.widget.TextView
import androidx.annotation.Px
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
@ -468,4 +471,9 @@ object Extensions {
downloadStartTime = timeInMillis
}
fun TextWithSubtitle(title: String, subtitle: String) : Spanned {
return HtmlCompat.fromHtml("<b><big>" + title + "</big></b>" + "<br />" +
"<small>" + subtitle + "</small>" + "<br />", HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}

View file

@ -0,0 +1,155 @@
package com.deniscerri.ytdl.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.ContextCompat
import androidx.core.content.contentValuesOf
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.google.gson.Gson
import com.google.gson.JsonObject
import org.json.JSONObject
import java.text.Normalizer.Form
import java.util.regex.Pattern
class FormatSorter(private var context: Context) {
private val sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val videoFormatIDPreference : List<String> = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
private val audioFormatIDPreference : List<String> = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
private val videoQualityPreference : String = sharedPreferences.getString("video_quality", "best").toString()
private val audioLanguagePreference : String = sharedPreferences.getString("audio_language", "").toString()
private val audioCodecPreference : String = sharedPreferences.getString("audio_codec", "").toString()
private val videoCodecPreference : String = sharedPreferences.getString("video_codec", "").toString()
private val audioContainerPreference : String = sharedPreferences.getString("audio_format", "").toString()
private val videoContainerPreference : String = sharedPreferences.getString("video_format", "").toString()
@SuppressLint("RestrictedApi")
private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values).filter { it.contains("_") }.map{ it.split("_")[0].dropLast(1) }.reversed()
private val preferSmallerFormats = sharedPreferences.getBoolean("prefer_smaller_formats", false)
@SuppressLint("RestrictedApi")
fun getAudioFormatImportance() : Set<String> {
val itemValues = context.getStringArray(R.array.format_importance_audio_values).toSet()
val orderPreferences = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!!.split(",").toMutableSet()
if (preferSmallerFormats) {
orderPreferences.add("smallsize")
}
return orderPreferences
}
@SuppressLint("RestrictedApi")
fun getVideoFormatImportance() : Set<String> {
val itemValues = context.getStringArray(R.array.format_importance_video_values).toSet()
val orderPreferences = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!.split(",").toMutableSet()
if (preferSmallerFormats) {
orderPreferences.add("smallsize")
}
return orderPreferences
}
@SuppressLint("RestrictedApi")
fun sortAudioFormats(formats: List<Format>) : List<Format> {
val orderPreferences = getAudioFormatImportance()
val fieldSorter: Comparator<Format> = Comparator { a, b ->
for (order in orderPreferences) {
when(order) {
"smallsize" -> {
(a.filesize).compareTo(b.filesize)
}
"id" -> {
(audioFormatIDPreference.contains(b.format_id)).compareTo(audioFormatIDPreference.contains(a.format_id))
}
"language" -> {
if (audioLanguagePreference.isBlank()) {
0
}
else {
(b.lang?.contains(audioLanguagePreference) == true).compareTo(a.lang?.contains(audioLanguagePreference) == true)
}
}
"codec" -> {
("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(b.acodec))
.compareTo("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(a.acodec))
}
"container" -> {
(audioContainerPreference == b.container).compareTo(audioContainerPreference == a.container)
}
}
}
0
}
return formats.sortedWith(fieldSorter)
}
@SuppressLint("RestrictedApi")
fun sortVideoFormats(formats: List<Format>): List<Format> {
val orderPreferences = getVideoFormatImportance()
val fieldSorter = object : Comparator<Format> {
override fun compare(a: Format, b: Format): Int {
for (order in orderPreferences) {
val comparison = when (order) {
"smallsize" -> {
val result = a.filesize.compareTo(b.filesize)
result
}
"id" -> {
videoFormatIDPreference.contains(b.format_id).compareTo(videoFormatIDPreference.contains(a.format_id))
}
"codec" -> {
"^(${videoCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(b.vcodec.uppercase())
.compareTo("^(${videoCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(a.vcodec.uppercase()))
}
"resolution" -> {
when (videoQualityPreference) {
"worst" -> {
b.format_note.contains("worst", ignoreCase = true)
.compareTo(a.format_note.contains("worst", ignoreCase = true))
}
"best" -> {
b.format_note.contains("best", ignoreCase = true)
.compareTo(a.format_note.contains("best", ignoreCase = true))
}
else -> {
val preferenceIndex = videoResolutionOrder.indexOfFirst { videoQualityPreference.contains(it, true) }
val aIndex = videoResolutionOrder.indexOfFirst { a.format_note.contains(it, ignoreCase = true) }
val bIndex = videoResolutionOrder.indexOfFirst { b.format_note.contains(it, ignoreCase = true) }
if (aIndex > preferenceIndex || bIndex > preferenceIndex) {
-1
}else if(aIndex == -1 && bIndex == -1){
-1
}else{
bIndex.compareTo(aIndex)
}
}
}
}
"no_audio" -> {
(b.acodec == "none" || b.acodec == "").compareTo(a.acodec == "none" || a.acodec == "")
}
"container" -> {
videoContainerPreference.equals(b.container, ignoreCase = true)
.compareTo(videoContainerPreference.equals(a.container, ignoreCase = true))
}
else -> 0
}
if (comparison != 0) return comparison
}
return 0
}
}
return formats.sortedWith(fieldSorter)
}
}

View file

@ -439,18 +439,26 @@ class InfoUtil(private val context: Context) {
return query!!
}
fun getFormats(url: String) : List<Format> {
fun getFormats(url: String, source : String? = null) : List<Format> {
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(url)
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
if (m.find() && formatSource == "piped"){
return try {
try {
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url)[0]
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
item!!.formats
if (res.length() == 0) {
return if (source != null) {
listOf()
}else {
getFormatsFromYTDL(url)
}
}else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
return item!!.formats
}
}catch(e: Exception) {
if (e is CancellationException) throw e
return getFormatsFromYTDL(url)
@ -505,7 +513,7 @@ class InfoUtil(private val context: Context) {
return parseYTDLFormats(jsonArray)
}
fun getFormatsMultiple(urls: List<String>, progress: (progress: List<Format>) -> Unit){
fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: List<Format>) -> Unit){
val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete()
urlsFile.createNewFile()
@ -513,8 +521,8 @@ class InfoUtil(private val context: Context) {
urlsFile.appendText(it+"\n")
}
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val allYoutubeLinks = urls.any {p.matcher(it).find() }
if (formatSource == "yt-dlp" || !allYoutubeLinks){
try {
@ -678,7 +686,7 @@ class InfoUtil(private val context: Context) {
}
}
var website = jsonObject.getStringByAny("ie_key", "extractor_key", "extractor")
var website = jsonObject.getStringByAny("extractor_key", "extractor","ie_key")
if (website == "Generic" || website == "HTML5MediaEmbed") website = jsonObject.getStringByAny("webpage_url_domain")
var playlistTitle = jsonObject.getStringByAny("playlist_title")
var playlistURL: String? = ""
@ -728,8 +736,6 @@ class InfoUtil(private val context: Context) {
val type = jsonObject.getStringByAny("_type")
if (type == "playlist" && playlistTitle.isEmpty()) {
playlistTitle = title
title = ""
author = ""
}
val res = ResultItem(0,
@ -758,32 +764,34 @@ class InfoUtil(private val context: Context) {
if (formatsInJSON != null) {
for (f in formatsInJSON.length() - 1 downTo 0){
val format = formatsInJSON.getJSONObject(f)
if (format.has("filesize")){
if (format.get("filesize") == "None"){
kotlin.runCatching {
if (format.get("filesize").toString() == "None") {
format.remove("filesize")
if (format.has("filesize_approx") && format.get("filesize_approx") != "None"){
format.put("filesize", format.getInt("filesize_approx"))
}else{
format.put("filesize", 0)
}
}
try{
val size = format.get("filesize").toString().toFloat()
format.remove("filesize")
format.put("filesize", size)
}catch (ignored: Exception){}
}
if (format.has("filesize_approx")){
if (format.get("filesize_approx") == "None"){
kotlin.runCatching {
if (format.get("filesize_approx").toString() == "None") {
format.remove("filesize_approx")
format.put("filesize_approx", 0)
}
}
kotlin.runCatching {
if (!format.has("filesize")) {
format.put("filesize", 0)
}
}
kotlin.runCatching {
if(format.get("format_note").toString() == "null"){
format.remove("format_note")
}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
Log.e("NOTE", format.toString())
if (formatProper.format_note == null) continue
if (format.has("format_note")){
if (!formatProper!!.format_note.contains("audio only", true)) {
formatProper.format_note = format.getString("format_note")
@ -947,14 +955,17 @@ class InfoUtil(private val context: Context) {
}
@SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
lateinit var request : YoutubeDLRequest
val matchFilter = mutableListOf<String>()
request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
YoutubeDLRequest(downloadItem.url)
}else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
if(downloadItem.playlistIndex == null){
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
addOption("--match-filter", "id~='${matchPortion}'")
matchFilter.add("id~='${matchPortion}'")
}else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
}
@ -964,7 +975,8 @@ class InfoUtil(private val context: Context) {
val type = downloadItem.type
val downDir : File
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
val canWrite = File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() || sharedPreferences.getBoolean("access_all_files", false)
if (!sharedPreferences.getBoolean("cache_downloads", true) && canWrite){
downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
request.addOption("--no-quiet")
request.addOption("--no-simulate")
@ -1066,7 +1078,7 @@ class InfoUtil(private val context: Context) {
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(120)))
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(180)))
}
@ -1092,7 +1104,7 @@ class InfoUtil(private val context: Context) {
filenameTemplate = if (filenameTemplate.isBlank()){
"%(section_title&{} |)s%(title).170B"
}else{
"%(section_title&{} |)s$filenameTemplate"
"%(section_title&{} |)s $filenameTemplate"
}
if (downloadItem.downloadSections.split(";").size > 1){
filenameTemplate = "%(autonumber)d. $filenameTemplate [%(section_start>%H%M%S)s]"
@ -1126,8 +1138,10 @@ class InfoUtil(private val context: Context) {
}else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
audioQualityId = "wa/w"
}else if(audioQualityId.contains("kbps_ytdlnisgeneric")){
request.addOption("--match-filter", "abr<=${audioQualityId.split("kbps")[0]}")
matchFilter.add("abr<=${audioQualityId.split("kbps")[0]}")
audioQualityId = ""
}else{
audioQualityId += "/ba/b"
}
@ -1271,7 +1285,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.videoPreferences.removeAudio) audioF = ""
if(audioF.contains("kbps_ytdlnisgeneric")){
request.addOption("--match-filter", "abr<=${audioF.split("kbps")[0]}")
matchFilter.add("abr<=${audioF.split("kbps")[0]}")
audioF = ""
}
@ -1452,6 +1466,10 @@ class InfoUtil(private val context: Context) {
else -> {}
}
if (matchFilter.isNotEmpty()) {
request.addOption("--match-filter", matchFilter.joinToString(" & "))
}
if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
val cache = File(FileUtil.getCachePath(context))
cache.mkdirs()

View file

@ -0,0 +1,136 @@
package com.deniscerri.ytdl.util
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.PopupMenu
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.core.view.isGone
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView
import java.util.*
object NavbarUtil {
lateinit var settings : SharedPreferences
fun init(context: Context){
settings = PreferenceManager.getDefaultSharedPreferences(context)
}
private var navItems = mapOf(
"Home" to R.id.homeFragment,
"History" to R.id.historyFragment,
"Downloads" to R.id.downloadQueueMainFragment,
"Terminal" to R.id.terminalActivity,
"More" to R.id.moreFragment
)
fun getStartFragmentId(context: Context) : Int {
val pref = settings.getString("start_destination", "")!!
val items = getDefaultNavBarItems(context)
return if (pref == "") {
R.id.homeFragment
}else {
items.firstOrNull { it.itemId == navItems[pref] }!!.itemId
}
}
fun setNavBarItems(items: List<MenuItem>, context: Context) {
val prefString = mutableListOf<String>()
val defaultNavBarItems = getDefaultNavBarItems(context)
items.forEach { newItem ->
val index = defaultNavBarItems.indexOfFirst { newItem.itemId == it.itemId }
prefString.add(if (newItem.isVisible) index.toString() else "-$index")
}
settings.edit().putString("navigation_bar", prefString.joinToString(",")).apply()
}
fun setStartFragment(itemId: Int) {
settings.edit().putString("start_destination", navItems.filter { it.value == itemId }.keys.first()).apply()
}
fun getNavBarItems(context: Context): List<MenuItem> {
val prefItems = try {
getNavBarPrefs()
} catch (e: Exception) {
Log.e("fail to parse nav items", e.toString())
return getDefaultNavBarItems(context)
}
val p = PopupMenu(context, null)
MenuInflater(context).inflate(R.menu.bottom_nav_menu, p.menu)
if (prefItems.size == p.menu.size()) {
val navBarItems = mutableListOf<MenuItem>()
prefItems.forEach {
navBarItems.add(
p.menu[it.replace("-", "").toInt()].apply {
this.isVisible = !it.contains("-")
}
)
}
return navBarItems
}
return getDefaultNavBarItems(context)
}
private fun getNavBarPrefs(): List<String> {
return settings
.getString("navigation_bar", "Home,History,More")!!
.split(",")
}
private fun getDefaultNavBarItems(context: Context): List<MenuItem> {
val p = PopupMenu(context, null)
MenuInflater(context).inflate(R.menu.bottom_nav_menu, p.menu)
val navBarItems = mutableListOf<MenuItem>()
p.menu.forEach {
navBarItems.add(it)
}
return navBarItems
}
fun NavigationBarView.applyNavBarStyle(): Int {
val labelVisibilityMode = when (
settings.getString("label_visibility", "always")
) {
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
"never" -> NavigationBarView.LABEL_VISIBILITY_UNLABELED
else -> NavigationBarView.LABEL_VISIBILITY_AUTO
}
this.labelVisibilityMode = labelVisibilityMode
val navBarItems = getNavBarItems(this.context)
val menuItems = mutableListOf<MenuItem>()
// remove the old items
navBarItems.forEach {
menuItems.add(
this.menu.findItem(it.itemId)
)
this.menu.removeItem(it.itemId)
}
navBarItems.forEach { navBarItem ->
if (navBarItem.isVisible) {
val menuItem = menuItems.first { it.itemId == navBarItem.itemId }
this.menu.add(
menuItem.groupId,
menuItem.itemId,
Menu.NONE,
menuItem.title
).icon = menuItem.icon
}
}
return getStartFragmentId(this.context)
}
}

View file

@ -424,7 +424,7 @@ class NotificationUtil(var context: Context) {
.setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()
.addAction(0, resources.getString(R.string.pause), pauseNotificationPendingIntent)
//.addAction(0, resources.getString(R.string.pause), pauseNotificationPendingIntent)
.addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent)
notificationManager.notify(id, notificationBuilder.build())
} catch (e: Exception) {
@ -469,6 +469,11 @@ class NotificationUtil(var context: Context) {
fun cancelDownloadNotification(id: Int) {
notificationManager.cancel(id)
}
fun cancelErroredNotification(id: Int) {
notificationManager.cancel(DOWNLOAD_ERRORED_NOTIFICATION_ID + id)
}
fun createMoveCacheFilesNotification(pendingIntent: PendingIntent?, downloadMiscChannelId: String): Notification {
val notificationBuilder = getBuilder(downloadMiscChannelId)

View file

@ -56,6 +56,7 @@ import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.RestoreAppDataItem
import com.deniscerri.ytdl.database.models.TemplateShortcut
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
@ -96,6 +97,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
@ -580,15 +582,15 @@ object UiUtil {
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
text = item.title.ifEmpty { item.playlistTitle.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } }
setOnLongClickListener{
text = item.title.ifEmpty { item.url.ifEmpty { item.playlistTitle.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } } }
setOnLongClickListener {
showFullTextDialog(context, text.toString(), context.getString(R.string.title))
true
}
}
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)?.apply {
text = item.author
setOnLongClickListener{
setOnLongClickListener {
showFullTextDialog(context, text.toString(), context.getString(R.string.author))
true
}
@ -761,15 +763,17 @@ object UiUtil {
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
text = item!!.title
setOnClickListener{
text = item!!.title.ifEmpty { item.url }
setOnLongClickListener{
showFullTextDialog(context, text.toString(), context.getString(R.string.title))
true
}
}
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)?.apply {
text = item!!.author
setOnClickListener{
setOnLongClickListener{
showFullTextDialog(context, text.toString(), context.getString(R.string.author))
true
}
}

View file

@ -70,7 +70,7 @@ class DownloadWorker(
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success()
// this is needed for observe sources call, so it wont create result items
val createResultItem = inputData.getBoolean("createResultItem", true)
//val createResultItem = inputData.getBoolean("createResultItem", true)
val confTmp = Configuration(context.resources.configuration)
val currLang = sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }.split("-")
@ -134,7 +134,7 @@ class DownloadWorker(
val downloadLocation = downloadItem.downloadPath
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito
val commandString = infoUtil.parseYTDLRequestString(request)
@ -162,6 +162,7 @@ class DownloadWorker(
val eventBus = EventBus.getDefault()
runCatching {
YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id))
val title: String = downloadItem.title.ifEmpty { downloadItem.url }
@ -242,8 +243,7 @@ class DownloadWorker(
FileUtil.deleteConfigFiles(request)
//put download in history
val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) {
if (!downloadItem.incognito) {
if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) {
handler.postDelayed({
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
@ -264,7 +264,7 @@ class DownloadWorker(
val historyItem = HistoryItem(0,
downloadItem.url,
downloadItem.title.ifEmpty { downloadItem.playlistTitle },
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.util.Log
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.CoroutineWorker
@ -54,7 +55,27 @@ class ObserveSourceWorker(
val foregroundInfo = ForegroundInfo(workerID, notification)
setForegroundAsync(foregroundInfo)
val res = runCatching { infoUtil.getFromYTDL(item.url).toList() }.getOrElse { listOf() }
val list = kotlin.runCatching {
infoUtil.getFromYTDL(item.url)
}.onFailure {
Log.e("observe", it.toString())
}.getOrElse { listOf() }
if (list.isNotEmpty() && item.syncWithSource && item.alreadyProcessedLinks.isNotEmpty()){
val processedLinks = item.alreadyProcessedLinks
val incomingLinks = list.map { it.url }
val linksNotPresentAnymore = processedLinks.filter { !incomingLinks.contains(it) }
linksNotPresentAnymore.forEach {
val historyItems = historyRepo.getAllByURL(it)
historyItems.filter { h -> h.type == item.downloadItemTemplate.type }.forEach { h ->
historyRepo.delete(h, true)
}
}
}
val res = list
.filter { result ->
//if first run and get new items only is preferred then dont get anything on first run
if (item.getOnlyNewUploads && item.runCount == 0){
@ -83,7 +104,6 @@ class ObserveSourceWorker(
}
val items = mutableListOf<DownloadItem>()
res.forEach {
val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java)
val downloadItem = Gson().fromJson(string, DownloadItem::class.java)
@ -116,9 +136,9 @@ class ObserveSourceWorker(
alarmScheduler.schedule()
}
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
}
// if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
// items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
// }
items.forEach {
it.status = DownloadRepository.Status.Queued.toString()

View file

@ -80,7 +80,7 @@ class TerminalDownloadWorker(
val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(commandPath)).canWrite()
if (!noCache){
request.addOption("-P", FileUtil.getCachePath(context) + "/TERMINAL/" + itemId)
request.addOption("-P", FileUtil.getCachePath(context) + "TERMINAL/" + itemId)
}else if (!request.hasOption("-P")){
request.addOption("-P", FileUtil.formatPath(commandPath))
}

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:colorAccent" android:pathData="M21.99,12.34C22,12.23 22,12.11 22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,5.17 3.93,9.43 8.96,9.95c-0.93,-0.73 -1.72,-1.64 -2.32,-2.68C5.9,18 4,15.22 4,12c0,-1.85 0.63,-3.55 1.69,-4.9l5.66,5.66c0.56,-0.4 1.17,-0.73 1.82,-1L7.1,5.69C8.45,4.63 10.15,4 12,4c4.24,0 7.7,3.29 7.98,7.45C20.69,11.67 21.37,11.97 21.99,12.34zM17,13c-3.18,0 -5.9,1.87 -7,4.5c1.1,2.63 3.82,4.5 7,4.5s5.9,-1.87 7,-4.5C22.9,14.87 20.18,13 17,13zM17,20c-1.38,0 -2.5,-1.12 -2.5,-2.5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5C19.5,18.88 18.38,20 17,20zM18.5,17.5c0,0.83 -0.67,1.5 -1.5,1.5s-1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5S18.5,16.67 18.5,17.5z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:colorAccent" android:pathData="M12,3L4,9v12h5v-7h6v7h5V9z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12.47,14L9,10.5l1.4,-1.41 2.07,2.08L17.6,6 19,7.41 12.47,14zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="?attr/colorAccent"
android:pathData="M11,39h7.5L18.5,26.5h11L29.5,39L37,39L37,19.5L24,9.75 11,19.5ZM8,42L8,18L24,6l16,12v24L26.5,42L26.5,29.5h-5L21.5,42ZM24,24.35Z" />
</vector>

View file

@ -165,22 +165,21 @@
app:layout_constraintVertical_bias="0.51"
>
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_resume"
style="?attr/materialIconButtonFilledStyle"
android:visibility="gone"
android:layout_width="wrap_content"
android:contentDescription="@string/resume"
android:layout_height="wrap_content"
app:backgroundTint="?attr/colorSurface"
app:cornerRadius="15dp"
app:icon="@drawable/exomedia_ic_play_arrow_white"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/output"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"
app:layout_constraintVertical_bias="0.51" />
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/active_download_resume"-->
<!-- style="?attr/materialIconButtonFilledStyle"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:contentDescription="@string/resume"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:backgroundTint="?attr/colorSurface"-->
<!-- app:cornerRadius="15dp"-->
<!-- app:icon="@drawable/exomedia_ic_play_arrow_white"-->
<!-- app:iconSize="30dp"-->
<!-- app:iconTint="?android:textColorPrimary"-->
<!-- app:layout_constraintBottom_toTopOf="@+id/output"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"-->
<!-- app:layout_constraintVertical_bias="0.51" />-->
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_delete"

View file

@ -88,17 +88,40 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:textAlignment="textStart"
android:singleLine="true"
android:text="@string/app_name"
android:textSize="15sp"
app:icon="@drawable/ic_link" />
android:layout_height="wrap_content">
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:singleLine="true"
android:text="@string/app_name"
android:textAlignment="textStart"
android:textSize="15sp"
app:icon="@drawable/ic_link"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_incognito"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bottomsheet_incognito"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:contentDescription="@string/incognito"
app:icon="@drawable/ic_nightly"
android:alpha="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -29,6 +29,7 @@
<androidx.media3.ui.PlayerView
android:id="@+id/video_view"
android:keepScreenOn="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -158,17 +158,42 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textAlignment="textStart"
android:singleLine="true"
android:text="@string/app_name"
android:textSize="15sp"
app:icon="@drawable/ic_link" />
android:layout_height="wrap_content">
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:singleLine="true"
android:text="@string/app_name"
android:textAlignment="textStart"
android:textSize="15sp"
app:icon="@drawable/ic_link"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_incognito"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bottomsheet_incognito"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:contentDescription="@string/incognito"
app:icon="@drawable/ic_nightly"
android:alpha="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView android:id="@+id/download_card_view"
<com.google.android.material.card.MaterialCardView xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/download_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
@ -39,6 +40,23 @@
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<com.google.android.material.button.MaterialButton
android:id="@+id/incognitoLabel"
style="?attr/materialIconButtonStyle"
android:layout_width="20dp"
android:height="20dp"
android:clickable="false"
android:focusable="false"
android:visibility="gone"
android:layout_margin="5dp"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_nightly"
app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="@+id/downloads_image_view"
app:layout_constraintTop_toTopOf="@+id/downloads_image_view" />
<TextView
android:id="@+id/duration"

View file

@ -166,6 +166,44 @@
android:orientation="vertical" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/formatSourceLinear"
android:orientation="vertical"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/format_source_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="10dp"
android:singleLine="false"
android:text="@string/format_source"
android:textSize="12sp" />
<RadioGroup
android:id="@+id/format_source_group"
android:layout_marginHorizontal="20dp"
android:checkedButton="@+id/radio_button_1"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content">
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@ -198,6 +236,8 @@
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,26 +2,107 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
<LinearLayout
android:id="@+id/active_queued_linear"
android:orientation="vertical"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
android:scrollbars="none"
tools:listitem="@layout/active_download_card"
app:layout_constraintHorizontal_bias="0.0"
android:paddingBottom="100dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView>
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/active_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/active_no_results"
layout="@layout/no_results" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume" />
</LinearLayout>
<LinearLayout
android:id="@+id/queue_linear"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:minHeight="200dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_marginTop="20dp"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/in_queue_label"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:textColor="?android:colorPrimary"
android:text="@string/in_queue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/drag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
app:drawableStartCompat="@drawable/ic_drag_handle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/filesize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingBottom="10dp"
android:text="@string/file_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queued_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
android:scrollbars="none"
android:paddingBottom="100dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -12,7 +12,7 @@
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/logs_toolbar"
android:id="@+id/downloads_toolbar"
android:elevation="0dp"
app:title="@string/download_queue"
app:menu="@menu/download_queue_menu"

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:focusable="true"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="0dp"
tools:gravity="start|center_vertical">
<ImageView
android:id="@+id/home"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:paddingHorizontal="10dp"
android:src="@drawable/ic_home_outlined"
tools:ignore="ContentDescription" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox"
style="@style/Widget.Material3.CompoundButton.CheckBox"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:gravity="center_vertical" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
tools:text="Item" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/drag_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:clickable="false"
android:focusable="false"
android:padding="16dp"
android:tintMode="src_in"
app:srcCompat="@drawable/ic_drag_handle"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
</LinearLayout>

View file

@ -493,6 +493,13 @@
android:checked="false"
android:text="@string/get_new_uploads"/>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/sync_with_source"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/sync_with_source"/>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/retry_missing_downloads"
android:layout_width="match_parent"

View file

@ -25,6 +25,7 @@
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
app:layout_constraintTop_toTopOf="parent"
app:resize_mode="fixed_height"
app:useDefaultControls="true"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="20dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/options_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -4,10 +4,22 @@
<item android:title="@string/home"
android:icon="@drawable/ic_home"
android:id="@+id/homeFragment"/>
<item android:title="@string/downloads"
android:icon="@drawable/ic_downloads"
android:id="@+id/historyFragment"/>
<item android:title="@string/more"
android:icon="@drawable/ic_more"
android:id="@+id/moreFragment"/>
<item android:title="@string/queue"
android:icon="@drawable/baseline_downloading_24"
android:visible="false"
android:id="@+id/downloadQueueMainFragment"/>
<item android:title="@string/terminal"
android:icon="@drawable/ic_terminal"
android:visible="false"
android:id="@+id/terminalActivity"/>
</menu>

View file

@ -22,6 +22,12 @@
android:icon="@drawable/baseline_folder_24"
app:showAsAction="always"/>
<item
android:id="@+id/incognito"
android:title="@string/incognito"
android:icon="@drawable/ic_nightly"
app:showAsAction="always"/>
<item
android:id="@+id/more"

View file

@ -28,13 +28,6 @@
app:showAsAction="ifRoom"
android:title="@string/move_bottom" />
<item
android:id="@+id/download"
android:title="@string/download"
android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/select_all"
android:title="@string/select_all"

View file

@ -36,6 +36,9 @@
<action
android:id="@+id/action_homeFragment_to_downloadsAlreadyExistDialog"
app:destination="@id/downloadsAlreadyExistDialog" />
<action
android:id="@+id/action_homeFragment_to_terminalActivity"
app:destination="@id/terminalActivity" />
</fragment>
<fragment
android:id="@+id/historyFragment"

View file

@ -384,8 +384,7 @@
<string name="minute">Menit</string>
<string name="second">Detik</string>
<string name="format_importance_note">Sesuaikan kepentingan elemen format. Urutan ini hanya akan digunakan ketika aplikasi mengambil format dalam kartu unduhan dan otomatis memilih format!</string>
<string name="format_importance_audio">Urutan Prioritas Format (Audio)</string>
<string name="format_importance_video">Urutan Prioritas Format (Video)</string>
<string name="format_importance">Urutan Prioritas Format</string>
<string name="no_audio">Video Tanpa Audio</string>
<string name="download_archive_folder">Folder Arsip Unduhan</string>
<string name="location">Lokasi</string>

View file

@ -397,4 +397,5 @@
<string name="location">Vendi</string>
<string name="enable_alarm_permission">Duhet të aktivizoni lejen SCHEDULE_EXACT_ALARM në cilësimet e aplikacionit.</string>
<string name="process_downloads_background">Shkarkimet janë ende duke u ngarkuar. Vazhdoni t\'i përpunoni ato në sfond dhe të filloni shkarkimin?</string>
<string name="queue">Rradha</string>
</resources>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="grid_size">3</integer>
<bool name="uses_side_nav">true</bool>
</resources>

View file

@ -1463,4 +1463,16 @@
<item>container</item>
</string-array>
<string-array name="label_visibility">
<item>@string/always</item>
<item>@string/selected</item>
<item>@string/never</item>
</string-array>
<string-array name="label_visibility_values">
<item>always</item>
<item>selected</item>
<item>never</item>
</string-array>
</resources>

View file

@ -2,4 +2,5 @@
<integer name="grid_size">1</integer>
<bool name="isTablet">false</bool>
<bool name="is_right_to_left">false</bool>
<bool name="uses_side_nav">false</bool>
</resources>

View file

@ -401,4 +401,11 @@
<string name="location">Location</string>
<string name="enable_alarm_permission">You need to enable the SCHEDULE_EXACT_ALARM permission in the app settings.</string>
<string name="process_downloads_background">"Downloads are still loading. Continue processing them in the background and start downloading? "</string>
<string name="navigation_bar">Navigation Bar</string>
<string name="label_visibility">Label Visibility</string>
<string name="always">Always</string>
<string name="sync_with_source">Sync With Source</string>
<string name="queue">Queue</string>
<string name="reset">Reset</string>
<string name="restore_info">Press \'Restore\' to merge the saved data with your current data.\nPress \'Reset\' to erase and only use the saved data in the file.</string>
</resources>

View file

@ -7,7 +7,7 @@
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_incognito"
android:icon="@drawable/ic_nightly"
android:key="incognito"
app:summary="@string/incognito_summary"
app:title="@string/incognito" />

View file

@ -69,7 +69,7 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/misc">
<PreferenceCategory android:title="YouTube">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
@ -84,7 +84,24 @@
android:summary="@string/api_key_summary"
app:title="@string/api_key" />
<ListPreference
android:defaultValue="yt-dlp"
android:entries="@array/formats_source"
android:entryValues="@array/formats_source_values"
android:icon="@drawable/baseline_manage_search_24"
app:key="formats_source"
app:useSimpleSummaryProvider="true"
app:title="@string/format_source" />
<Preference
android:icon="@drawable/ic_dns"
app:key="piped_instance"
app:defaultValue=""
android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/misc">
<ListPreference
android:defaultValue=""
android:entries="@array/countries"
@ -95,15 +112,29 @@
app:key="locale"
app:title="@string/preferred_locale" />
<ListPreference
<Preference
android:defaultValue="Home"
android:entries="@array/start_destination"
android:entryValues="@array/start_destination_values"
app:icon="@drawable/baseline_add_to_home_screen_24"
app:useSimpleSummaryProvider="true"
app:key="start_destination"
app:isPreferenceVisible="false"
app:title="@string/preferred_home_screen" />
<Preference
android:defaultValue="Home,History,More"
android:icon="@drawable/baseline_home_filled_24"
android:key="navigation_bar"
android:title="@string/preferred_home_screen" />
<ListPreference
android:defaultValue="always"
android:entries="@array/label_visibility"
android:entryValues="@array/label_visibility_values"
android:icon="@drawable/baseline_disabled_visible_24"
app:key="label_visibility"
app:useSimpleSummaryProvider="true"
app:title="@string/label_visibility" />
<ListPreference
android:defaultValue="ytsearch"
android:entries="@array/search_engines"
@ -157,4 +188,5 @@
app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -33,15 +33,9 @@
android:key="auto_update_ytdlp"
app:title="@string/auto_update_ytdlp" />
<ListPreference
android:defaultValue="yt-dlp"
android:entries="@array/formats_source"
android:entryValues="@array/formats_source_values"
android:icon="@drawable/baseline_manage_search_24"
app:key="formats_source"
app:useSimpleSummaryProvider="true"
app:title="@string/format_source" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/format">
<ListPreference
android:defaultValue="filesize"
android:entries="@array/format_ordering"
@ -67,14 +61,6 @@
android:key="update_formats"
app:summary="@string/update_formats_summary"
app:title="@string/update_formats" />
<Preference
android:icon="@drawable/ic_dns"
app:key="piped_instance"
app:defaultValue=""
android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/app">

View file

@ -24,7 +24,7 @@ buildscript {
retrofitVer = "2.9.0"
kodeinVer = "7.16.0"
navVer = "2.7.7"
media3_version = "1.2.1"
media3_version = "1.3.1"
agp_version = '8.1.0'
agp_version1 = '7.4.2'
roomVer = '2.5.2'