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 properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 7 def versionPatch = 8
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def isBeta = false def isBeta = false
def versionExt = isBeta ? ".${versionBuild}-beta" : "" def versionExt = isBeta ? ".${versionBuild}-beta" : ""
@ -130,6 +130,10 @@ dependencies {
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$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.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -142,6 +146,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5' implementation 'androidx.test.ext:junit-ktx:1.1.5'
implementation 'androidx.compose.ui:ui-android:1.6.5' implementation 'androidx.compose.ui:ui-android:1.6.5'
implementation 'androidx.preference:preference:1.2.1'
testImplementation "junit:junit:$junitVer" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -177,7 +182,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' 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" implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer // 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.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.HomeFragment 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.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.util.CrashListener import com.deniscerri.ytdl.util.CrashListener
import com.deniscerri.ytdl.util.FileUtil 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.ThemeUtil
import com.deniscerri.ytdl.util.UpdateUtil import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
@ -125,26 +129,39 @@ class MainActivity : BaseActivity() {
} }
} }
if (savedInstanceState == null){ NavbarUtil.init(this)
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
}
if (navigationView is NavigationBarView){ 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).setupWithNavController(navController)
(navigationView as NavigationBarView).setOnItemReselectedListener { (navigationView as NavigationBarView).setOnItemReselectedListener {
when (it.itemId) { when (it.itemId) {
R.id.homeFragment -> { R.id.homeFragment -> {
(navHostFragment.childFragmentManager.primaryNavigationFragment!! as HomeFragment).scrollToTop() kotlin.runCatching {
(navHostFragment.childFragmentManager.primaryNavigationFragment!! as HomeFragment).scrollToTop()
}
} }
R.id.historyFragment -> { 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 -> { R.id.moreFragment -> {
val intent = Intent(context, SettingsActivity::class.java) 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 { lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest { downloadViewModel.activeDownloadsCount.collectLatest {
if (it == 0) { if (it == 0) {
@ -220,12 +241,14 @@ class MainActivity : BaseActivity() {
(navigationView as NavigationView).getHeaderView(0).findViewById<TextView>(R.id.title).text = ThemeUtil.getStyledAppName(this) (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, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (navigationView is NavigationBarView){ if (navigationView is NavigationBarView){
when(destination.id){ if (showingNavbarItems.contains(destination.id)) {
R.id.homeFragment, R.id.historyFragment, R.id.moreFragment -> showBottomNavigation() showBottomNavigation()
else -> hideBottomNavigation() }else{
hideBottomNavigation()
} }
} }
} }

View file

@ -41,7 +41,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem
TerminalItem::class, TerminalItem::class,
ObserveSourcesItem::class ObserveSourcesItem::class
], ],
version = 17, version = 19,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), 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 = 13, to = 14) MANUALLY HANDLED
AutoMigration (from = 14, to = 15), AutoMigration (from = 14, to = 15),
AutoMigration (from = 15, to = 16, spec = Migrations.resetObserveSources::class), 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(){ 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)") 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( @DeleteTable.Entries(
@ -39,5 +43,4 @@ object Migrations {
} }
} }

View file

@ -83,6 +83,9 @@ interface CommandTemplateDao {
@Query("DELETE FROM templateShortcuts WHERE id=:itemId") @Query("DELETE FROM templateShortcuts WHERE id=:itemId")
suspend fun deleteShortcut(itemId: Long) suspend fun deleteShortcut(itemId: Long)
@Query("DELETE FROM templateShortcuts")
suspend fun deleteAllShortcuts()
@Update @Update
suspend fun update(item: CommandTemplate) 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") @Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem fun getFirstProcessingDownload() : DownloadItem
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem> fun getProcessingDownloadsList() : List<DownloadItem>
@ -63,12 +61,12 @@ interface DownloadDao {
suspend fun updateProcessingDownloadPath(path: String) suspend fun updateProcessingDownloadPath(path: String)
@Query("SELECT * FROM downloads WHERE status='Active'") @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')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status in ('Active', 'QueuedPaused', 'ActivePaused')") @Query("UPDATE downloads SET status='Queued' where status = 'Active'")
suspend fun resetActivePausedItems() suspend fun resetActiveToQueued()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued')") @Query("SELECT id FROM downloads WHERE status in('Active','Queued')")
fun getActiveAndQueuedDownloadIDs() : List<Long> fun getActiveAndQueuedDownloadIDs() : List<Long>
@ -102,8 +100,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloadsList() : List<DownloadItem> fun getCancelledDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple> fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple>
@ -159,6 +155,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Error'") @Query("DELETE FROM downloads WHERE status='Error'")
suspend fun deleteErrored() suspend fun deleteErrored()
@Query("DELETE FROM downloads WHERE status='Queued'")
suspend fun deleteQueued()
@Query("DELETE FROM downloads WHERE status='Saved'") @Query("DELETE FROM downloads WHERE status='Saved'")
suspend fun deleteSaved() suspend fun deleteSaved()
@ -269,10 +268,16 @@ interface DownloadDao {
@Query("Update downloads set id=:newId where id=:id") @Query("Update downloads set id=:newId where id=:id")
suspend fun updateDownloadID(id: Long, newId: Long) 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") @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> 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") @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> 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 = "") @ColumnInfo(defaultValue = "")
var playlistURL: String? = "", var playlistURL: String? = "",
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null var playlistIndex: Int? = null,
@ColumnInfo(defaultValue = "0")
var incognito: Boolean = false
) : Parcelable ) : Parcelable

View file

@ -22,4 +22,5 @@ data class DownloadItemSimple(
var type: DownloadViewModel.Type, var type: DownloadViewModel.Type,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
var downloadStartTime: Long, 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, var retryMissingDownloads: Boolean,
@ColumnInfo(defaultValue = "[]") @ColumnInfo(defaultValue = "[]")
var ignoredLinks: MutableList<String>, var ignoredLinks: MutableList<String>,
var alreadyProcessedLinks : MutableList<String> var alreadyProcessedLinks : MutableList<String>,
@ColumnInfo(defaultValue = "0")
var syncWithSource: Boolean
) : Parcelable ) : Parcelable

View file

@ -60,6 +60,10 @@ class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
commandDao.deleteShortcut(item.id) commandDao.deleteShortcut(item.id)
} }
suspend fun deleteAllShortcuts() {
commandDao.deleteAllShortcuts()
}
suspend fun deleteAll(){ suspend fun deleteAll(){
commandDao.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()) val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status { enum class Status {
Active, ActivePaused, Queued, Error, Cancelled, Saved, Processing, Scheduled Active, Queued, Error, Cancelled, Saved, Processing, Scheduled
} }
suspend fun insert(item: DownloadItem) : Long { suspend fun insert(item: DownloadItem) : Long {
@ -118,12 +118,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getDownloadsByIds(ids) return downloadDao.getDownloadsByIds(ids)
} }
fun getAllItemsByIDsFlow(ids: List<Long>) : Flow<List<DownloadItem>> {
return downloadDao.getDownloadsByIdsFlow(ids)
}
fun getActiveDownloads() : List<DownloadItem> { fun getActiveDownloads() : List<DownloadItem> {
return downloadDao.getActiveAndPausedDownloadsList() return downloadDao.getActiveDownloadsList()
} }
fun getProcessingDownloads() : List<DownloadItem> { fun getProcessingDownloads() : List<DownloadItem> {
@ -154,10 +150,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getCancelledDownloadsList() return downloadDao.getCancelledDownloadsList()
} }
fun getPausedDownloads() : List<DownloadItem> {
return downloadDao.getPausedDownloadsList()
}
fun getErroredDownloads() : List<DownloadItem> { fun getErroredDownloads() : List<DownloadItem> {
return downloadDao.getErroredDownloadsList() return downloadDao.getErroredDownloadsList()
} }
@ -182,6 +174,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
deleteCache(errored) deleteCache(errored)
} }
suspend fun deleteQueued() {
downloadDao.deleteQueued()
}
suspend fun deleteSaved(){ suspend fun deleteSaved(){
downloadDao.deleteSaved() downloadDao.deleteSaved()
} }
@ -238,7 +234,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val workConstraints = Constraints.Builder() val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else { else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED) //workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
} }
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
@ -268,12 +264,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}, 0) }, 0)
} }
val first = queuedItems.first() if (queuedItems.isNotEmpty()) {
if (first.downloadStartTime > 0L) { val first = queuedItems.first()
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime) if (first.downloadStartTime > 0L) {
handler.postDelayed({ val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime)
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show() handler.postDelayed({
}, 0) 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) { class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
private val tag: String = "ResultRepository" private val tag: String = "ResultRepository"
val YTDLNIS_SEARCH = "YTDLNIS_SEARCH"
val allResults : Flow<List<ResultItem>> = resultDao.getResults() val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1) var itemCount = MutableStateFlow(-1)
@ -56,7 +57,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll() deleteAll()
itemCount.value = v.size itemCount.value = v.size
}else{ }else{
v.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = "ytdlnis-Search" } v.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = YTDLNIS_SEARCH }
} }
if (addToResults){ if (addToResults){
val ids = resultDao.insertMultiple(v) val ids = resultDao.insertMultiple(v)
@ -99,7 +100,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll() deleteAll()
itemCount.value = items.size itemCount.value = items.size
}else{ }else{
items.filter { it.playlistTitle.isNullOrBlank() }.forEach { it.playlistTitle = "ytdlnis-Search" } items.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = YTDLNIS_SEARCH }
} }
if (addToResults){ 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() val info = getResultsFromSource(downloadItem.url, resetResults = false, addToResults = false, singleItem = true).first()
if (downloadItem.title.isEmpty()) downloadItem.title = info.title if (downloadItem.title.isEmpty()) downloadItem.title = info.title
if (downloadItem.author.isEmpty()) downloadItem.author = info.author 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.duration = info.duration
downloadItem.website = info.website downloadItem.website = info.website
if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info.thumb if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info.thumb

View file

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

View file

@ -76,21 +76,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>> 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 extraCommandsForAudio: String = ""
private var extraCommandsForVideo: 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 dao: DownloadDao
private val historyRepository: HistoryRepository private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository 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) { fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) {
@ -325,14 +282,26 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
container, container,
"", "",
ArrayList(), 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> { fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
return sharedDownloadViewModel.getPreferredVideoRequirements() return sharedDownloadViewModel.getPreferredAudioRequirements()
} }
//requirement and importance //requirement and importance
@ -371,7 +340,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
try { try {
itemIDs.forEachIndexed { idx, it -> itemIDs.forEachIndexed { idx, it ->
val item = repository.getItemByID(it) val item = repository.getItemByID(it)
if (!isActive) throw CancellationException() if (processingItemsFlow?.job?.isCancelled == true) throw CancellationException()
item.id = 0 item.id = 0
item.status = DownloadRepository.Status.Processing.toString() item.status = DownloadRepository.Status.Processing.toString()
@ -383,11 +352,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
processingItems.emit(false) processingItems.emit(false)
} catch (e: Exception) { } catch (e: Exception) {
processingItemsFlow?.apply { deleteProcessing()
this.processingDownloadItemIDs.chunked(100).forEach {
repository.deleteAllWithIDs(it)
}
}
updateProcessingJobData(null) updateProcessingJobData(null)
processingItems.emit(false) 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) { fun turnResultItemsToProcessingDownloads(itemIDs: List<Long>, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs)) updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
processingItems.emit(true) processingItems.emit(true)
try { try {
itemIDs.forEach { id -> itemIDs.forEach { id ->
@ -409,7 +375,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
)) ))
downloadItem.status = DownloadRepository.Status.Processing.toString() downloadItem.status = DownloadRepository.Status.Processing.toString()
if (!isActive) { if (processingItemsFlow?.job?.isCancelled == true) {
throw CancellationException() throw CancellationException()
} }
@ -425,11 +391,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
processingItems.emit(false) processingItems.emit(false)
}catch (e: Exception) { }catch (e: Exception) {
processingItemsFlow?.apply { deleteProcessing()
this.processingDownloadItemIDs.chunked(100).forEach {
repository.deleteAllWithIDs(it)
}
}
updateProcessingJobData(null) updateProcessingJobData(null)
processingItems.emit(false) processingItems.emit(false)
} }
@ -472,6 +434,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteErrored() repository.deleteErrored()
} }
fun deleteQueued() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteQueued()
}
fun deleteSaved() = viewModelScope.launch(Dispatchers.IO) { fun deleteSaved() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteSaved() repository.deleteSaved()
} }
@ -513,7 +479,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getActiveDownloadsCount() : Int { 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 { fun getQueuedDownloadsCount() : Int {
@ -529,8 +499,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun resetActivePaused() { suspend fun resetActiveToQueued() {
dbManager.downloadDao.resetActivePausedItems() dbManager.downloadDao.resetActiveToQueued()
} }
suspend fun startDownloadWorker(list: List<DownloadItem>){ suspend fun startDownloadWorker(list: List<DownloadItem>){
@ -751,5 +721,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getScheduledIDsBetweenTwoItems(item1, item2) 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.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.google.gson.Gson import com.google.gson.Gson
@ -230,13 +231,18 @@ class SharedDownloadViewModel(private val context: Context) {
resultItem.formats, resultItem.formats,
downloadPath!!, resultItem.website, downloadPath!!, resultItem.website,
"", "",
resultItem.playlistTitle, if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle,
audioPreferences, audioPreferences,
videoPreferences, videoPreferences,
extraCommands, extraCommands,
customFileNameTemplate!!, customFileNameTemplate!!,
saveThumb, 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 requirements: MutableList<(Format) -> Int> = mutableListOf()
val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet() 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){ for(i in 0..preferenceIndex){
removeAt(0) removeAt(0)
} }
requirements.add { it: Format -> if (it.format_note.contains(preference, ignoreCase = true)) importance else 0 } add(0, preference)
forEachIndexed { index, res -> 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 } 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 ( return cloneFormat (
try { try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" } val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
val requirements = getPreferredAudioRequirements() FormatSorter(context).sortAudioFormats(theFormats).first()
theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception() //
// val requirements = getPreferredAudioRequirements()
// theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
}catch (e: Exception){ }catch (e: Exception){
bestAudioFormat bestAudioFormat
} }
@ -395,24 +402,27 @@ class SharedDownloadViewModel(private val context: Context) {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty { val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
defaultVideoFormats.sortedByDescending { it.filesize } defaultVideoFormats.sortedByDescending { it.filesize }
} }
when (videoQualityPreference) {
"worst" -> { FormatSorter(context).sortVideoFormats(theFormats).first()
theFormats.last() //
} // when (videoQualityPreference) {
else /*best*/ -> { // "worst" -> {
val requirements = getPreferredVideoRequirements() // theFormats.last()
theFormats.run { // }
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){ // else /*best*/ -> {
sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception() // val requirements = getPreferredVideoRequirements()
}else{ // theFormats.run {
sortedByDescending { it.filesize }.maxByOrNull { f -> // if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
val summ = requirements.sumOf { req -> req(f) } // sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
summ // }else{
} ?: throw Exception() // sortedByDescending { it.filesize }.maxByOrNull { f ->
} // val summ = requirements.sumOf { req -> req(f) }
} // summ
} // } ?: throw Exception()
} // }
// }
// }
// }
}catch (e: Exception){ }catch (e: Exception){
bestVideoFormat bestVideoFormat
} }

View file

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

View file

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

View file

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

View file

@ -88,6 +88,10 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
formatNote.visibility = View.GONE 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 codec = card.findViewById<TextView>(R.id.codec)
val codecText = val codecText =
if (item.format.encoding != "") { if (item.format.encoding != "") {
@ -160,7 +164,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
return oldItem.title == newItem.title && return oldItem.title == newItem.title &&
oldItem.author == newItem.author && oldItem.author == newItem.author &&
oldItem.type == newItem.type && 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) val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE 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 androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem 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.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -36,21 +34,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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 tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2 private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter private lateinit var fragmentAdapter : DownloadFragmentAdapter
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener
private lateinit var sharedPreferences : SharedPreferences private lateinit var sharedPreferences : SharedPreferences
private var incognito: Boolean = currentDownloadItem.incognito
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
onDownloadItemUpdateListener = listener onDownloadItemUpdateListener = listener
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) 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 //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){ if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
@ -111,7 +107,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
fragmentAdapter = DownloadFragmentAdapter( fragmentAdapter = DownloadFragmentAdapter(
fragmentManager, fragmentManager,
lifecycle, lifecycle,
result, null,
currentDownloadItem currentDownloadItem
) )
viewPager2.adapter = fragmentAdapter viewPager2.adapter = fragmentAdapter
@ -194,24 +190,39 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
val ok = view.findViewById<Button>(R.id.bottom_sheet_ok) val ok = view.findViewById<Button>(R.id.bottom_sheet_ok)
ok!!.setOnClickListener { ok!!.setOnClickListener {
val item = getDownloadItem() val item = getDownloadItem()
onDownloadItemUpdateListener.onDownloadItemUpdate(result.id, item) onDownloadItemUpdateListener.onDownloadItemUpdate(item)
dismiss() dismiss()
} }
val link = view.findViewById<Button>(R.id.bottom_sheet_link) val link = view.findViewById<Button>(R.id.bottom_sheet_link)
link.text = result.url link.text = currentDownloadItem.url
link.setOnClickListener{ link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), result.url) UiUtil.openLinkIntent(requireContext(), currentDownloadItem.url)
} }
link.setOnLongClickListener{ link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), result.url) UiUtil.copyLinkToClipBoard(requireContext(), currentDownloadItem.url)
true 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{ private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
return when(selectedTabPosition){ val item = when(selectedTabPosition){
0 -> { 0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem.apply { id = currentDownloadItem.id } f.downloadItem.apply { id = currentDownloadItem.id }
@ -225,6 +236,8 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
f.downloadItem.apply { id = currentDownloadItem.id } f.downloadItem.apply { id = currentDownloadItem.id }
} }
} }
item.incognito = incognito
return item
} }
@ -261,7 +274,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
interface OnDownloadItemUpdateListener { interface OnDownloadItemUpdateListener {
fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) fun onDownloadItemUpdate(item: DownloadItem)
} }
override fun onCancel(dialog: DialogInterface) { override fun onCancel(dialog: DialogInterface) {

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -17,7 +18,9 @@ import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -85,6 +88,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var result: ResultItem private lateinit var result: ResultItem
private lateinit var type: Type private lateinit var type: Type
private var currentDownloadItem: DownloadItem? = null private var currentDownloadItem: DownloadItem? = null
private var incognito: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -113,6 +117,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
} }
result = res result = res
currentDownloadItem = dwl currentDownloadItem = dwl
incognito = currentDownloadItem?.incognito ?: sharedPreferences.getBoolean("incognito", false)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -424,6 +429,20 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
(updateItem.parent as LinearLayout).visibility = View.GONE (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 //update in the background if there is no data
if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false) && type != Type.command){ 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 { private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem {
return when(selectedTabPosition){ val item = when(selectedTabPosition){
0 -> { 0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem f.downloadItem
@ -618,6 +637,9 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
f.downloadItem f.downloadItem
} }
} }
item.incognito = incognito
return item
} }
private fun getAlsoAudioDownloadItem(finished: (it: DownloadItem) -> Unit) { 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 = view.findViewById(R.id.freespace)
freeSpace.text = String.format(getString(R.string.freespace) + ": " + FileUtil.convertFileSize( val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace 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){ val shortcutCount = withContext(Dispatchers.IO){
commandTemplateViewModel.getTotalShortcutNumber() commandTemplateViewModel.getTotalShortcutNumber()

View file

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

View file

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

View file

@ -125,10 +125,8 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
override fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int) { override fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int) {
val resultItem = downloadViewModel.createResultItemFromDownload(alreadyExistsItem.downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener { val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate( override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem item: DownloadItem
) { ) {
val currentIndex = duplicates.indexOf(alreadyExistsItem) val currentIndex = duplicates.indexOf(alreadyExistsItem)
@ -138,7 +136,7 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
} }
} }
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, alreadyExistsItem.downloadItem, onItemUpdated) val bottomSheet = ConfigureDownloadBottomSheetDialog(alreadyExistsItem.downloadItem, onItemUpdated)
bottomSheet.show(requireActivity().supportFragmentManager, "configureDownloadSingleSheet") 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
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout 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.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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() { 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 formats: List<List<Format>>
private lateinit var listener: OnFormatClickListener private lateinit var listener: OnFormatClickListener
private var currentFormatSource : String? = null
enum class FormatSorting { enum class FormatSorting {
filesize, container, codec, id filesize, container, codec, id
} }
@ -157,6 +162,31 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
val refreshBtn = view.findViewById<Button>(R.id.format_refresh) val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
if (!hasGenericFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) refreshBtn.visibility = View.GONE 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 { refreshBtn.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
@ -172,6 +202,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
chosenFormats = emptyList() chosenFormats = emptyList()
refreshBtn.isEnabled = false refreshBtn.isEnabled = false
formatSourceLinear.isVisible = false
formatListLinearLayout.visibility = View.GONE formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE shimmers.visibility = View.VISIBLE
shimmers.startShimmer() shimmers.startShimmer()
@ -182,7 +213,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
formatCollection.clear() formatCollection.clear()
kotlin.runCatching { kotlin.runCatching {
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
infoUtil.getFormats(items.first()!!.url) infoUtil.getFormats(items.first()!!.url, currentFormatSource)
} }
res.filter { it.format_note != "storyboard" } res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) { chosenFormats = if (items.first()?.type == Type.audio) {
@ -215,7 +246,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
refreshBtn.text = progress refreshBtn.text = progress
} }
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
infoUtil.getFormatsMultiple(items.map { it!!.url }) { infoUtil.getFormatsMultiple(items.map { it!!.url }, currentFormatSource) {
if (isActive) { if (isActive) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
formatCollection.add(it) formatCollection.add(it)
@ -259,6 +290,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
shimmers.stopShimmer() shimmers.stopShimmer()
formatSourceLinear.isVisible = canSwitch
filterBtn.isVisible = !hasGenericFormats filterBtn.isVisible = !hasGenericFormats
addFormatsToView() addFormatsToView()
refreshBtn.isVisible = hasGenericFormats refreshBtn.isVisible = hasGenericFormats
@ -271,6 +303,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
refreshBtn.text = getString(R.string.update_formats) refreshBtn.text = getString(R.string.update_formats)
formatListLinearLayout.visibility = View.VISIBLE formatListLinearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
formatSourceLinear.isVisible = canSwitch
shimmers.stopShimmer() shimmers.stopShimmer()
e.printStackTrace() e.printStackTrace()
@ -393,23 +426,16 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
FormatSorting.filesize -> chosenFormats FormatSorting.filesize -> chosenFormats
} }
val formatSorter = FormatSorter(requireContext())
//filter category //filter category
when(filterBy){ when(filterBy){
FormatCategory.ALL -> {} FormatCategory.ALL -> {}
FormatCategory.SUGGESTED -> { FormatCategory.SUGGESTED -> {
finalFormats = if (items.first()?.type == Type.audio){ finalFormats = if (items.first()?.type == Type.audio){
val req = downloadViewModel.getPreferredAudioRequirements() formatSorter.sortAudioFormats(finalFormats)
finalFormats
.filter { f -> req.sumOf{req -> req(f)} >= 1 }
.sortedByDescending { f -> req.sumOf{req -> req(f)} }
.ifEmpty { listOf(downloadViewModel.getFormat(finalFormats, Type.audio)) }
}else{ }else{
val req = downloadViewModel.getPreferredVideoRequirements() formatSorter.sortVideoFormats(finalFormats)
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)) }
} }
} }
FormatCategory.SMALLEST -> { 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.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel 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.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -98,6 +99,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var endsAfterNr: TextInputLayout private lateinit var endsAfterNr: TextInputLayout
private lateinit var retryMissingDownloads: MaterialSwitch private lateinit var retryMissingDownloads: MaterialSwitch
private lateinit var getOnlyNewUploads: MaterialSwitch private lateinit var getOnlyNewUploads: MaterialSwitch
private lateinit var syncWithSource: MaterialSwitch
private lateinit var resetProcessedLinks: MaterialSwitch private lateinit var resetProcessedLinks: MaterialSwitch
@ -262,9 +264,14 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfterNr = view.findViewById(R.id.after_nr) endsAfterNr = view.findViewById(R.id.after_nr)
retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads) retryMissingDownloads = view.findViewById(R.id.retry_missing_downloads)
getOnlyNewUploads = view.findViewById(R.id.get_new_uploads) getOnlyNewUploads = view.findViewById(R.id.get_new_uploads)
syncWithSource = view.findViewById(R.id.sync_with_source)
resetProcessedLinks = view.findViewById(R.id.reset_processed_links) resetProcessedLinks = view.findViewById(R.id.reset_processed_links)
okButton = view.findViewById(R.id.okButton) 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!!.setText(currentItem?.name ?: "")
title.editText?.doAfterTextChanged { checkIfValid() } title.editText?.doAfterTextChanged { checkIfValid() }
@ -469,6 +476,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
retryMissingDownloads.isChecked = currentItem?.retryMissingDownloads ?: false retryMissingDownloads.isChecked = currentItem?.retryMissingDownloads ?: false
getOnlyNewUploads.isChecked = currentItem?.getOnlyNewUploads ?: false getOnlyNewUploads.isChecked = currentItem?.getOnlyNewUploads ?: false
syncWithSource.isChecked = currentItem?.syncWithSource ?: false
resetProcessedLinks.isVisible = currentItem != null resetProcessedLinks.isVisible = currentItem != null
if (currentItem != null) okButton.text = getString(R.string.update) if (currentItem != null) okButton.text = getString(R.string.update)
@ -518,6 +526,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfterCount = endsAfterCount, endsAfterCount = endsAfterCount,
runCount = 0, runCount = 0,
getOnlyNewUploads = getOnlyNewUploads.isChecked, getOnlyNewUploads = getOnlyNewUploads.isChecked,
syncWithSource = syncWithSource.isChecked,
retryMissingDownloads = retryMissingDownloads.isChecked, retryMissingDownloads = retryMissingDownloads.isChecked,
alreadyProcessedLinks = if (resetProcessedLinks.isChecked){ alreadyProcessedLinks = if (resetProcessedLinks.isChecked){
mutableListOf() mutableListOf()

View file

@ -597,47 +597,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
cancelActiveDownload(itemID) 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() { override fun onCardClick() {
this.dismiss() this.dismiss()
findNavController().navigate( 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){ private fun cancelActiveDownload(itemID: Long){
lifecycleScope.launch { lifecycleScope.launch {
val id = itemID.toInt() val id = itemID.toInt()

View file

@ -1,14 +1,27 @@
package com.deniscerri.ytdl.ui.downloads package com.deniscerri.ytdl.ui.downloads
import android.animation.AnimatorSet
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView 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.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -16,21 +29,36 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter 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.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.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.DownloadWorker 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.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -42,14 +70,27 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener { class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var activeDownloadsLinear: LinearLayout
private lateinit var activeDownloadsNoResults: View
private lateinit var activeRecyclerView : RecyclerView private lateinit var activeRecyclerView : RecyclerView
private lateinit var activeDownloads : ActiveDownloadAdapter 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 notificationUtil: NotificationUtil
private lateinit var pause: ExtendedFloatingActionButton private lateinit var pause: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton private lateinit var resume: ExtendedFloatingActionButton
@ -67,7 +108,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
return fragmentView return fragmentView
} }
@SuppressLint("NotifyDataSetChanged") @OptIn(ExperimentalBadgeUtils::class)
@SuppressLint("NotifyDataSetChanged", "RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -77,12 +119,34 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager = WorkManager.getInstance(requireContext()) workManager = WorkManager.getInstance(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
activeDownloadsLinear = view.findViewById(R.id.active_linear)
activeDownloadsNoResults = view.findViewById(R.id.active_no_results)
activeDownloads = ActiveDownloadAdapter(this,requireActivity()) activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview) activeRecyclerView = view.findViewById(R.id.download_recyclerview)
activeRecyclerView.forceFastScrollMode() activeRecyclerView.forceFastScrollMode()
activeRecyclerView.adapter = activeDownloads activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) 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 = view.findViewById(R.id.pause)
pause.isVisible = false pause.isVisible = false
resume = view.findViewById(R.id.resume) resume = view.findViewById(R.id.resume)
@ -91,56 +155,123 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pause.setOnClickListener { pause.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
workManager.cancelAllWorkByTag("download") workManager.cancelAllWorkByTag("download")
withContext(Dispatchers.IO){ val activeDownloadsList = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads() downloadViewModel.getActiveDownloads()
}.forEach { }
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString()) YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
} }
preferences.edit().putBoolean("paused_downloads", true).apply() preferences.edit().putBoolean("paused_downloads", true).apply()
pause.isEnabled = false
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
resume.isVisible = true withContext(Dispatchers.Main){
pause.isVisible = false delay(1000)
pause.isVisible = false
pause.isEnabled = true
resume.isVisible = true
}
} }
} }
resume.setOnClickListener { resume.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
preferences.edit().putBoolean("paused_downloads", false).apply() preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false resume.isEnabled = false
pause.isVisible = true
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadViewModel.resetActivePaused() downloadViewModel.resetActiveToQueued()
downloadViewModel.startDownloadWorker(listOf()) downloadViewModel.startDownloadWorker(listOf())
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
activeDownloads.notifyDataSetChanged() 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 { lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest { downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued)).observe(viewLifecycleOwner){
noResults.isVisible = it == 0 queuedDownloadsLinear.isVisible = it > 0
val pausedDownloads = preferences.getBoolean("paused_downloads", false) queuedTotalSize = it
pause.isVisible = it > 0 && !pausedDownloads 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 { queuedDragHandleToggle.setOnClickListener {
downloadViewModel.activeQueuedDownloadsCount.collectLatest { queuedDownloads.toggleShowDragHandle()
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
resume.isVisible = it > 0 && pausedDownloads
}
}
lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest {
delay(100)
activeDownloads.submitList(it)
}
} }
} }
//dont remove
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress") 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() { override fun onStart() {
super.onStart() super.onStart()
EventBus.getDefault().register(this) 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) { override fun onOutputClick(item: DownloadItem) {
if (item.logID != null && item.logID != 0L) { if (item.logID != null && item.logID != 0L) {
val bundle = Bundle() 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 package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -16,6 +17,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -29,7 +31,9 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel 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.Extensions.createBadge
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
@ -52,6 +56,7 @@ class DownloadQueueMainFragment : Fragment(){
private lateinit var viewPager2: ViewPager2 private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadListFragmentAdapter private lateinit var fragmentAdapter : DownloadListFragmentAdapter
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var notificationUtil: NotificationUtil
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
override fun onCreateView( override fun onCreateView(
@ -60,6 +65,7 @@ class DownloadQueueMainFragment : Fragment(){
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
mainActivity = activity as MainActivity mainActivity = activity as MainActivity
notificationUtil = NotificationUtil(mainActivity)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
return inflater.inflate(R.layout.fragment_download_queue_main_screen, container, false) return inflater.inflate(R.layout.fragment_download_queue_main_screen, container, false)
} }
@ -69,7 +75,11 @@ class DownloadQueueMainFragment : Fragment(){
workManager = WorkManager.getInstance(requireContext()) workManager = WorkManager.getInstance(requireContext())
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] 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() } topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
tabLayout = view.findViewById(R.id.download_tablayout) tabLayout = view.findViewById(R.id.download_tablayout)
@ -80,7 +90,7 @@ class DownloadQueueMainFragment : Fragment(){
overScrollMode = View.OVER_SCROLL_NEVER 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( fragmentAdapter = DownloadListFragmentAdapter(
childFragmentManager, childFragmentManager,
@ -94,11 +104,11 @@ class DownloadQueueMainFragment : Fragment(){
TabLayoutMediator(tabLayout, viewPager2) { tab, position -> TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
when (position) { when (position) {
0 -> tab.text = getString(R.string.running) 0 -> tab.text = getString(R.string.running)
1 -> tab.text = getString(R.string.in_queue) // 1 -> tab.text = getString(R.string.in_queue)
2 -> tab.text = getString(R.string.scheduled) 1 -> tab.text = getString(R.string.scheduled)
3 -> tab.text = getString(R.string.cancelled) 2 -> tab.text = getString(R.string.cancelled)
4 -> tab.text = getString(R.string.errored) 3 -> tab.text = getString(R.string.errored)
5 -> tab.text = getString(R.string.saved) 4 -> tab.text = getString(R.string.saved)
} }
}.attach() }.attach()
@ -124,11 +134,12 @@ class DownloadQueueMainFragment : Fragment(){
initMenu() initMenu()
if (arguments?.getString("tab") != null){ if (arguments?.getString("tab") != null){
tabLayout.getTabAt(4)!!.select() tabLayout.getTabAt(3)!!.select()
viewPager2.postDelayed( { viewPager2.postDelayed( {
viewPager2.setCurrentItem(4, false) viewPager2.setCurrentItem(3, false)
val reconfigureID = arguments?.getLong("reconfigure") val reconfigureID = arguments?.getLong("reconfigure")
reconfigureID?.apply { reconfigureID?.apply {
notificationUtil.cancelErroredNotification(this.toInt())
lifecycleScope.launch { lifecycleScope.launch {
kotlin.runCatching { kotlin.runCatching {
val item = withContext(Dispatchers.IO){ 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 { lifecycleScope.launch {
downloadViewModel.queuedDownloadsCount.collectLatest { downloadViewModel.scheduledDownloadsCount.collectLatest {
tabLayout.getTabAt(1)?.apply { tabLayout.getTabAt(1)?.apply {
createBadge(it) createBadge(it)
} }
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.scheduledDownloadsCount.collectLatest { downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(2)?.apply { tabLayout.getTabAt(2)?.apply {
createBadge(it) createBadge(it)
} }
} }
} }
lifecycleScope.launch {
downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(3)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest { downloadViewModel.erroredDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply { tabLayout.getTabAt(3)?.apply {
removeBadge() removeBadge()
if (it > 0) createBadge(it) if (it > 0) createBadge(it)
} }
@ -186,7 +197,7 @@ class DownloadQueueMainFragment : Fragment(){
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest { downloadViewModel.savedDownloadsCount.collectLatest {
tabLayout.getTabAt(5)?.apply { tabLayout.getTabAt(4)?.apply {
removeBadge() removeBadge()
if (it > 0) createBadge(it) if (it > 0) createBadge(it)
} }
@ -264,7 +275,10 @@ class DownloadQueueMainFragment : Fragment(){
deleteDialog.show() deleteDialog.show()
} }
@SuppressLint("NotifyDataSetChanged")
private fun cancelAllDownloads() { private fun cancelAllDownloads() {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
fragmentAdapter.notifyDataSetChanged()
workManager.cancelAllWorkByTag("download") workManager.cancelAllWorkByTag("download")
lifecycleScope.launch { lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext()) val notificationUtil = NotificationUtil(requireContext())
@ -278,4 +292,9 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.cancelActiveQueued() 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.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.ui.adapter.HistoryAdapter import com.deniscerri.ytdl.ui.adapter.HistoryAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
@ -200,8 +201,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
initChips() initChips()
} }
private fun scrollToTop() { fun scrollToTop() {
recyclerView!!.scrollToPosition(0) recyclerView.scrollToPosition(0)
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
(topAppBar!!.parent as AppBarLayout).setExpanded( (topAppBar!!.parent as AppBarLayout).setExpanded(
true, true,
@ -239,6 +240,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
}) })
topAppBar!!.setOnClickListener { scrollToTop() } 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 -> topAppBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
R.id.remove_history -> { R.id.remove_history -> {
@ -402,7 +405,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
val websites = mutableListOf<String>() val websites = mutableListOf<String>()
val websiteFilter = historyViewModel.websiteFilter.value val websiteFilter = historyViewModel.websiteFilter.value
for (item in list){ 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() websiteGroup!!.removeAllViews()
if (websites.size <= 1) { if (websites.size <= 1) {

View file

@ -112,11 +112,8 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
override fun onActionButtonClick(itemID: Long) { override fun onActionButtonClick(itemID: Long) {
lifecycleScope.launch { lifecycleScope.launch {
runCatching { runCatching {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(listOf(item.id)) downloadViewModel.resetScheduleTimeForItemsAndStartDownload(listOf(itemID))
} }
}.onFailure { }.onFailure {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_LONG).show() 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.view.ViewGroup
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope 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.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdl.util.NavbarUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -63,6 +65,9 @@ class MoreFragment : Fragment() {
terminateApp = view.findViewById(R.id.terminate) terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings) 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 { terminal.setOnClickListener {
val intent = Intent(context, TerminalActivity::class.java) val intent = Intent(context, TerminalActivity::class.java)
startActivity(intent) startActivity(intent)
@ -76,6 +81,9 @@ class MoreFragment : Fragment() {
findNavController().navigate(R.id.commandTemplatesFragment) findNavController().navigate(R.id.commandTemplatesFragment)
} }
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
downloadQueue.isVisible = !showingDownloadQueue
downloadQueue.setOnClickListener { downloadQueue.setOnClickListener {
findNavController().navigate(R.id.downloadQueueMainFragment) 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.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.core.view.forEach
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat 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.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R 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.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.Locale import java.util.Locale
@ -36,13 +52,17 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
private var showTerminalShareIcon: SwitchPreferenceCompat? = null private var showTerminalShareIcon: SwitchPreferenceCompat? = null
private var ignoreBatteryOptimization: Preference? = null private var ignoreBatteryOptimization: Preference? = null
private var displayOverApps: SwitchPreferenceCompat? = null private var displayOverApps: SwitchPreferenceCompat? = null
private lateinit var preferences: SharedPreferences
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0 private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.general_preferences, rootKey) setPreferencesFromResource(R.xml.general_preferences, rootKey)
NavbarUtil.init(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
updateUtil = UpdateUtil(requireContext()) updateUtil = UpdateUtil(requireContext())
val editor = preferences.edit()
WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){ WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){
activeDownloadCount = 0 activeDownloadCount = 0
@ -66,6 +86,90 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
true 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!!.summary = theme!!.entry
theme!!.onPreferenceChangeListener = theme!!.onPreferenceChangeListener =
@ -153,6 +257,14 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
startActivity(intent) startActivity(intent)
true true
} }
findPreference<Preference>("piped_instance")?.setOnPreferenceClickListener {
UiUtil.showPipedInstancesDialog(requireActivity(), preferences.getString("piped_instance", "")!!){
editor.putString("piped_instance", it)
editor.apply()
}
true
}
} }
override fun onResume() { override fun onResume() {
@ -164,6 +276,15 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
super.onResume() 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( private var displayOverAppsResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
@ -171,5 +292,4 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
} }
} }

View file

@ -1,17 +1,24 @@
package com.deniscerri.ytdl.ui.more.settings package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.LayoutDirection import android.util.LayoutDirection
import android.util.Log import android.util.Log
import android.view.Gravity
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
@ -24,6 +31,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.BuildConfig import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R 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.CookieItem
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.HistoryItem 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.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.models.SearchHistoryItem import com.deniscerri.ytdl.database.models.SearchHistoryItem
import com.deniscerri.ytdl.database.models.TemplateShortcut 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.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -418,11 +431,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
} }
lifecycleScope.launch { lifecycleScope.launch {
val preferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
runCatching { runCatching {
val ip = requireContext().contentResolver.openInputStream(result.data!!.data!!) val ip = requireContext().contentResolver.openInputStream(result.data!!.data!!)
val r = BufferedReader(InputStreamReader(ip)) val r = BufferedReader(InputStreamReader(ip))
@ -432,308 +441,148 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
total.append(line).append('\n') total.append(line).append('\n')
} }
//PARSE RESTORE JSON
val json = Gson().fromJson(total.toString(), JsonObject::class.java) val json = Gson().fromJson(total.toString(), JsonObject::class.java)
val finalMessage = StringBuilder("") val restoreData = RestoreAppDataItem()
val errorMessage = StringBuilder("") val parsedDataMessage = StringBuilder()
//preference restore if (json.has("settings")) {
kotlin.runCatching { restoreData.settings = json.getAsJsonArray("settings")
if(json.has("settings")){ parsedDataMessage.appendLine("${getString(R.string.settings)}: ${restoreData.settings!!.size()}")
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}")
} }
//history restore if (json.has("downloads")) {
kotlin.runCatching { restoreData.downloads = json.getAsJsonArray("downloads").map {
if(json.has("downloads")){ val item =
val items = json.getAsJsonArray("downloads") Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java)
items.forEach { item.id = 0L
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java) item
item.id = 0L
withContext(Dispatchers.IO){
historyViewModel.insert(item)
}
}
finalMessage.append("${getString(R.string.downloads)}: ${items.count()}\n")
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.downloads)}: ${restoreData.downloads!!.size}")
errorMessage.append("\n HISTORY RESTORE FAILED: \n ${it.message}")
} }
//queued downloads restore if (json.has("queued")) {
kotlin.runCatching { restoreData.queued = json.getAsJsonArray("queued").map {
if(json.has("queued")){ val item =
val items = json.getAsJsonArray("queued") Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
val queued = mutableListOf<DownloadItem>() item.id = 0L
items.forEach { item
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
queued.add(item)
}
if (queued.isNotEmpty()) {
finalMessage.append("${getString(R.string.in_queue)}: ${queued.count()}\n")
withContext(Dispatchers.IO){downloadViewModel.queueDownloads(queued)}
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.queue)}: ${restoreData.queued!!.size}")
errorMessage.append("\n QUEUED DOWNLOADS RESTORE FAILED: \n ${it.message}")
} }
//scheduled downloads restore if (json.has("scheduled")) {
kotlin.runCatching { restoreData.scheduled = json.getAsJsonArray("scheduled").map {
if(json.has("scheduled")){ val item =
val items = json.getAsJsonArray("scheduled") Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
val scheduled = mutableListOf<DownloadItem>() item.id = 0L
items.forEach { item
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)}
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.scheduled)}: ${restoreData.scheduled!!.size}")
errorMessage.append("\n SCHEDULED DOWNLOADS RESTORE FAILED: \n ${it.message}")
} }
//cancelled downloads restore if (json.has("cancelled")) {
kotlin.runCatching { restoreData.cancelled = json.getAsJsonArray("cancelled").map {
if(json.has("cancelled")){ val item =
val items = json.getAsJsonArray("cancelled") Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
val cancelled = mutableListOf<DownloadItem>() item.id = 0L
items.forEach { item
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")
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.cancelled)}: ${restoreData.cancelled!!.size}")
errorMessage.append("\n CANCELLED DOWNLOADS RESTORE FAILED: \n ${it.message}")
} }
//erorred downloads restore if (json.has("errored")) {
kotlin.runCatching { restoreData.errored = json.getAsJsonArray("errored").map {
if(json.has("errored")){ val item =
val items = json.getAsJsonArray("errored") Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
val errored = mutableListOf<DownloadItem>() item.id = 0L
items.forEach { item
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")
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.errored)}: ${restoreData.errored!!.size}")
errorMessage.append("\n ERRORED DOWNLOADS RESTORE FAILED: \n ${it.message}")
} }
//saved downloads restore if (json.has("saved")) {
kotlin.runCatching { restoreData.saved = json.getAsJsonArray("saved").map {
if(json.has("saved")){ val item =
val items = json.getAsJsonArray("saved") Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
val saved = mutableListOf<DownloadItem>() item.id = 0L
items.forEach { item
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")
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.saved)}: ${restoreData.saved!!.size}")
errorMessage.append("\n SAVED DOWNLOADS RESTORE FAILED: \n ${it.message}")
} }
//cookies restore if (json.has("cookies")) {
kotlin.runCatching { restoreData.cookies = json.getAsJsonArray("cookies").map {
if(json.has("cookies")){ val item =
val items = json.getAsJsonArray("cookies") Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java)
items.forEach { item.id = 0L
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java) item
item.id = 0L
withContext(Dispatchers.IO){
cookieViewModel.insert(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.cookies)}: ${items.count()}\n")
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.cookies)}: ${restoreData.cookies!!.size}")
errorMessage.append("\n COOKIES RESTORE FAILED: \n ${it.message}")
} }
//command template restore if (json.has("templates")) {
kotlin.runCatching { restoreData.templates = json.getAsJsonArray("templates").map {
if(json.has("templates")){ val item = Gson().fromJson(
val items = json.getAsJsonArray("templates") it.toString().replace("^\"|\"$", ""),
val templates = mutableListOf<CommandTemplate>() CommandTemplate::class.java
items.forEach { )
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java) item.id = 0L
item.id = 0L item
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")
}
} }
}.onFailure { parsedDataMessage.appendLine("${getString(R.string.command_templates)}: ${restoreData.templates!!.size}")
errorMessage.append("\n COMMAND TEMPLATE RESTORE FAILED: \n ${it.message}")
} }
//shortcuts restore if (json.has("shortcuts")) {
kotlin.runCatching { restoreData.shortcuts = json.getAsJsonArray("shortcuts").map {
if(json.has("shortcuts")){ val item = Gson().fromJson(
val items = json.getAsJsonArray("shortcuts") it.toString().replace("^\"|\"$", ""),
items.forEach { TemplateShortcut::class.java
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), TemplateShortcut::class.java) )
item.id = 0L item.id = 0L
withContext(Dispatchers.IO){ item
commandTemplateViewModel.insertShortcut(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.shortcuts)}: ${items.count()}\n")
}
} }
}.onFailure {
errorMessage.append("\n COMMAND SHORTCUTS RESTORE FAILED: \n ${it.message}") parsedDataMessage.appendLine("${getString(R.string.shortcuts)}: ${restoreData.shortcuts!!.size}")
} }
if (json.has("search_history")) {
//search history restore restoreData.searchHistory = json.getAsJsonArray("search_history").map {
kotlin.runCatching { val item = Gson().fromJson(
if(json.has("search_history")){ it.toString().replace("^\"|\"$", ""),
val items = json.getAsJsonArray("search_history") SearchHistoryItem::class.java
items.forEach { )
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), SearchHistoryItem::class.java) item.id = 0L
withContext(Dispatchers.IO){ item
resultViewModel.addSearchQueryToHistory(item.query)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.search_history)}: ${items.count()}\n")
}
} }
}.onFailure {
errorMessage.append("\n SEARCH HISTORY RESTORE FAILED: \n ${it.message}") parsedDataMessage.appendLine("${getString(R.string.search_history)}: ${restoreData.searchHistory!!.size}")
} }
//observe sources restore if (json.has("observe_sources")) {
kotlin.runCatching { restoreData.observeSources = json.getAsJsonArray("observe_sources").map {
if(json.has("observe_sources")){ val item = Gson().fromJson(
val items = json.getAsJsonArray("observe_sources") it.toString().replace("^\"|\"$", ""),
items.forEach { ObserveSourcesItem::class.java
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), ObserveSourcesItem::class.java) )
withContext(Dispatchers.IO){ item.id = 0L
item.id = 0L item
observeSourcesViewModel.insert(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("") showAppRestoreInfoDialog(
onMerge = {
val builder = MaterialAlertDialogBuilder(requireContext()) restoreData(restoreData,parsedDataMessage.toString())
builder.setTitle(getString(R.string.restore)) },
builder.setMessage("${getString(R.string.restore_complete)}\n $finalMessage \n $errorMessage") onReset = {
builder.setPositiveButton( restoreData(restoreData,parsedDataMessage.toString(), true)
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")))
} }
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 { }.onFailure {
it.printStackTrace() it.printStackTrace()
Snackbar.make(requireView(), getString(R.string.couldnt_parse_file), Snackbar.LENGTH_LONG).show() 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 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 { findPreference<Preference>("changelog")?.setOnPreferenceClickListener {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {

View file

@ -17,6 +17,8 @@ import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri import android.net.Uri
import android.text.Html
import android.text.Spanned
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.TypedValue import android.util.TypedValue
import android.view.MotionEvent import android.view.MotionEvent
@ -30,6 +32,7 @@ import android.widget.TextView
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted import androidx.lifecycle.withStarted
@ -468,4 +471,9 @@ object Extensions {
downloadStartTime = timeInMillis 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!! 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 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"){ if (m.find() && formatSource == "piped"){
return try { try {
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id") val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url)[0] if (res.length() == 0) {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id") return if (source != null) {
item!!.formats listOf()
}else {
getFormatsFromYTDL(url)
}
}else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
return item!!.formats
}
}catch(e: Exception) { }catch(e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
return getFormatsFromYTDL(url) return getFormatsFromYTDL(url)
@ -505,7 +513,7 @@ class InfoUtil(private val context: Context) {
return parseYTDLFormats(jsonArray) 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") val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete() urlsFile.delete()
urlsFile.createNewFile() urlsFile.createNewFile()
@ -513,8 +521,8 @@ class InfoUtil(private val context: Context) {
urlsFile.appendText(it+"\n") urlsFile.appendText(it+"\n")
} }
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp") val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?") val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val allYoutubeLinks = urls.any {p.matcher(it).find() } val allYoutubeLinks = urls.any {p.matcher(it).find() }
if (formatSource == "yt-dlp" || !allYoutubeLinks){ if (formatSource == "yt-dlp" || !allYoutubeLinks){
try { 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") if (website == "Generic" || website == "HTML5MediaEmbed") website = jsonObject.getStringByAny("webpage_url_domain")
var playlistTitle = jsonObject.getStringByAny("playlist_title") var playlistTitle = jsonObject.getStringByAny("playlist_title")
var playlistURL: String? = "" var playlistURL: String? = ""
@ -728,8 +736,6 @@ class InfoUtil(private val context: Context) {
val type = jsonObject.getStringByAny("_type") val type = jsonObject.getStringByAny("_type")
if (type == "playlist" && playlistTitle.isEmpty()) { if (type == "playlist" && playlistTitle.isEmpty()) {
playlistTitle = title playlistTitle = title
title = ""
author = ""
} }
val res = ResultItem(0, val res = ResultItem(0,
@ -758,32 +764,34 @@ class InfoUtil(private val context: Context) {
if (formatsInJSON != null) { if (formatsInJSON != null) {
for (f in formatsInJSON.length() - 1 downTo 0){ for (f in formatsInJSON.length() - 1 downTo 0){
val format = formatsInJSON.getJSONObject(f) val format = formatsInJSON.getJSONObject(f)
if (format.has("filesize")){ kotlin.runCatching {
if (format.get("filesize") == "None"){ if (format.get("filesize").toString() == "None") {
format.remove("filesize") 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")){ kotlin.runCatching {
if (format.get("filesize_approx") == "None"){ if (format.get("filesize_approx").toString() == "None") {
format.remove("filesize_approx") 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) val formatProper = Gson().fromJson(format.toString(), Format::class.java)
Log.e("NOTE", format.toString())
if (formatProper.format_note == null) continue if (formatProper.format_note == null) continue
if (format.has("format_note")){ if (format.has("format_note")){
if (!formatProper!!.format_note.contains("audio only", true)) { if (!formatProper!!.format_note.contains("audio only", true)) {
formatProper.format_note = format.getString("format_note") formatProper.format_note = format.getString("format_note")
@ -947,14 +955,17 @@ class InfoUtil(private val context: Context) {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){ lateinit var request : YoutubeDLRequest
val matchFilter = mutableListOf<String>()
request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
YoutubeDLRequest(downloadItem.url) YoutubeDLRequest(downloadItem.url)
}else{ }else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply { YoutubeDLRequest(downloadItem.playlistURL!!).apply {
if(downloadItem.playlistIndex == null){ if(downloadItem.playlistIndex == null){
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first() val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
addOption("--match-filter", "id~='${matchPortion}'") matchFilter.add("id~='${matchPortion}'")
}else{ }else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}") addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
} }
@ -964,7 +975,8 @@ class InfoUtil(private val context: Context) {
val type = downloadItem.type val type = downloadItem.type
val downDir : File 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)) downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
request.addOption("--no-quiet") request.addOption("--no-quiet")
request.addOption("--no-simulate") request.addOption("--no-simulate")
@ -1066,7 +1078,7 @@ class InfoUtil(private val context: Context) {
} }
if(downloadItem.title.isNotBlank()){ 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()){ filenameTemplate = if (filenameTemplate.isBlank()){
"%(section_title&{} |)s%(title).170B" "%(section_title&{} |)s%(title).170B"
}else{ }else{
"%(section_title&{} |)s$filenameTemplate" "%(section_title&{} |)s $filenameTemplate"
} }
if (downloadItem.downloadSections.split(";").size > 1){ if (downloadItem.downloadSections.split(";").size > 1){
filenameTemplate = "%(autonumber)d. $filenameTemplate [%(section_start>%H%M%S)s]" 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)){ }else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
audioQualityId = "wa/w" audioQualityId = "wa/w"
}else if(audioQualityId.contains("kbps_ytdlnisgeneric")){ }else if(audioQualityId.contains("kbps_ytdlnisgeneric")){
request.addOption("--match-filter", "abr<=${audioQualityId.split("kbps")[0]}") matchFilter.add("abr<=${audioQualityId.split("kbps")[0]}")
audioQualityId = "" audioQualityId = ""
}else{
audioQualityId += "/ba/b"
} }
@ -1271,7 +1285,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.videoPreferences.removeAudio) audioF = "" if (downloadItem.videoPreferences.removeAudio) audioF = ""
if(audioF.contains("kbps_ytdlnisgeneric")){ if(audioF.contains("kbps_ytdlnisgeneric")){
request.addOption("--match-filter", "abr<=${audioF.split("kbps")[0]}") matchFilter.add("abr<=${audioF.split("kbps")[0]}")
audioF = "" audioF = ""
} }
@ -1452,6 +1466,10 @@ class InfoUtil(private val context: Context) {
else -> {} else -> {}
} }
if (matchFilter.isNotEmpty()) {
request.addOption("--match-filter", matchFilter.joinToString(" & "))
}
if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){ if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
val cache = File(FileUtil.getCachePath(context)) val cache = File(FileUtil.getCachePath(context))
cache.mkdirs() 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) .setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions() .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) .addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent)
notificationManager.notify(id, notificationBuilder.build()) notificationManager.notify(id, notificationBuilder.build())
} catch (e: Exception) { } catch (e: Exception) {
@ -469,6 +469,11 @@ class NotificationUtil(var context: Context) {
fun cancelDownloadNotification(id: Int) { fun cancelDownloadNotification(id: Int) {
notificationManager.cancel(id) notificationManager.cancel(id)
} }
fun cancelErroredNotification(id: Int) {
notificationManager.cancel(DOWNLOAD_ERRORED_NOTIFICATION_ID + id)
}
fun createMoveCacheFilesNotification(pendingIntent: PendingIntent?, downloadMiscChannelId: String): Notification { fun createMoveCacheFilesNotification(pendingIntent: PendingIntent?, downloadMiscChannelId: String): Notification {
val notificationBuilder = getBuilder(downloadMiscChannelId) 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.DownloadItem
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.HistoryItem 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.models.TemplateShortcut
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
@ -96,6 +97,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -580,15 +582,15 @@ object UiUtil {
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet) bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply { bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
text = item.title.ifEmpty { item.playlistTitle.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } } text = item.title.ifEmpty { item.url.ifEmpty { item.playlistTitle.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } } }
setOnLongClickListener{ setOnLongClickListener {
showFullTextDialog(context, text.toString(), context.getString(R.string.title)) showFullTextDialog(context, text.toString(), context.getString(R.string.title))
true true
} }
} }
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)?.apply { bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)?.apply {
text = item.author text = item.author
setOnLongClickListener{ setOnLongClickListener {
showFullTextDialog(context, text.toString(), context.getString(R.string.author)) showFullTextDialog(context, text.toString(), context.getString(R.string.author))
true true
} }
@ -761,15 +763,17 @@ object UiUtil {
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet) bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply { bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
text = item!!.title text = item!!.title.ifEmpty { item.url }
setOnClickListener{ setOnLongClickListener{
showFullTextDialog(context, text.toString(), context.getString(R.string.title)) showFullTextDialog(context, text.toString(), context.getString(R.string.title))
true
} }
} }
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)?.apply { bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)?.apply {
text = item!!.author text = item!!.author
setOnClickListener{ setOnLongClickListener{
showFullTextDialog(context, text.toString(), context.getString(R.string.author)) 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() 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 // 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 confTmp = Configuration(context.resources.configuration)
val currLang = sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }.split("-") val currLang = sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }.split("-")
@ -134,7 +134,7 @@ class DownloadWorker(
val downloadLocation = downloadItem.downloadPath val downloadLocation = downloadItem.downloadPath
val keepCache = sharedPreferences.getBoolean("keep_cache", false) 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) val commandString = infoUtil.parseYTDLRequestString(request)
@ -162,6 +162,7 @@ class DownloadWorker(
val eventBus = EventBus.getDefault() val eventBus = EventBus.getDefault()
runCatching { runCatching {
YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id)) eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id))
val title: String = downloadItem.title.ifEmpty { downloadItem.url } val title: String = downloadItem.title.ifEmpty { downloadItem.url }
@ -242,8 +243,7 @@ class DownloadWorker(
FileUtil.deleteConfigFiles(request) FileUtil.deleteConfigFiles(request)
//put download in history //put download in history
val incognito = sharedPreferences.getBoolean("incognito", false) if (!downloadItem.incognito) {
if (!incognito) {
if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) { if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) {
handler.postDelayed({ handler.postDelayed({
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
@ -264,7 +264,7 @@ class DownloadWorker(
val historyItem = HistoryItem(0, val historyItem = HistoryItem(0,
downloadItem.url, downloadItem.url,
downloadItem.title.ifEmpty { downloadItem.playlistTitle }, downloadItem.title,
downloadItem.author, downloadItem.author,
downloadItem.duration, downloadItem.duration,
downloadItem.thumb, downloadItem.thumb,

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdl.work package com.deniscerri.ytdl.work
import android.content.Context import android.content.Context
import android.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
@ -54,7 +55,27 @@ class ObserveSourceWorker(
val foregroundInfo = ForegroundInfo(workerID, notification) val foregroundInfo = ForegroundInfo(workerID, notification)
setForegroundAsync(foregroundInfo) 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 -> .filter { result ->
//if first run and get new items only is preferred then dont get anything on first run //if first run and get new items only is preferred then dont get anything on first run
if (item.getOnlyNewUploads && item.runCount == 0){ if (item.getOnlyNewUploads && item.runCount == 0){
@ -83,7 +104,6 @@ class ObserveSourceWorker(
} }
val items = mutableListOf<DownloadItem>() val items = mutableListOf<DownloadItem>()
res.forEach { res.forEach {
val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java) val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java)
val downloadItem = Gson().fromJson(string, DownloadItem::class.java) val downloadItem = Gson().fromJson(string, DownloadItem::class.java)
@ -116,9 +136,9 @@ class ObserveSourceWorker(
alarmScheduler.schedule() alarmScheduler.schedule()
} }
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){ // if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" } // items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
} // }
items.forEach { items.forEach {
it.status = DownloadRepository.Status.Queued.toString() 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() val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(commandPath)).canWrite()
if (!noCache){ if (!noCache){
request.addOption("-P", FileUtil.getCachePath(context) + "/TERMINAL/" + itemId) request.addOption("-P", FileUtil.getCachePath(context) + "TERMINAL/" + itemId)
}else if (!request.hasOption("-P")){ }else if (!request.hasOption("-P")){
request.addOption("-P", FileUtil.formatPath(commandPath)) 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" app:layout_constraintVertical_bias="0.51"
> >
<com.google.android.material.button.MaterialButton <!-- <com.google.android.material.button.MaterialButton-->
android:id="@+id/active_download_resume" <!-- android:id="@+id/active_download_resume"-->
style="?attr/materialIconButtonFilledStyle" <!-- style="?attr/materialIconButtonFilledStyle"-->
android:visibility="gone" <!-- android:layout_width="wrap_content"-->
android:layout_width="wrap_content" <!-- android:contentDescription="@string/resume"-->
android:contentDescription="@string/resume" <!-- android:layout_height="wrap_content"-->
android:layout_height="wrap_content" <!-- app:backgroundTint="?attr/colorSurface"-->
app:backgroundTint="?attr/colorSurface" <!-- app:cornerRadius="15dp"-->
app:cornerRadius="15dp" <!-- app:icon="@drawable/exomedia_ic_play_arrow_white"-->
app:icon="@drawable/exomedia_ic_play_arrow_white" <!-- app:iconSize="30dp"-->
app:iconSize="30dp" <!-- app:iconTint="?android:textColorPrimary"-->
app:iconTint="?android:textColorPrimary" <!-- app:layout_constraintBottom_toTopOf="@+id/output"-->
app:layout_constraintBottom_toTopOf="@+id/output" <!-- app:layout_constraintEnd_toEndOf="parent"-->
app:layout_constraintEnd_toEndOf="parent" <!-- app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"-->
app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview" <!-- app:layout_constraintVertical_bias="0.51" />-->
app:layout_constraintVertical_bias="0.51" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/active_download_delete" android:id="@+id/active_download_delete"

View file

@ -88,17 +88,40 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<Button <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_marginHorizontal="10dp"
android:textAlignment="textStart" <Button
android:singleLine="true" android:id="@+id/bottom_sheet_link"
android:text="@string/app_name" style="@style/Widget.Material3.Button.TextButton.Icon"
android:textSize="15sp" android:layout_width="0dp"
app:icon="@drawable/ic_link" /> 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> </LinearLayout>

View file

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

View file

@ -158,17 +158,42 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<Button <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_margin="10dp"
android:textAlignment="textStart" <Button
android:singleLine="true" android:id="@+id/bottom_sheet_link"
android:text="@string/app_name" style="@style/Widget.Material3.Button.TextButton.Icon"
android:textSize="15sp" android:layout_width="0dp"
app:icon="@drawable/ic_link" /> 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent" android:backgroundTint="@android:color/transparent"
@ -39,6 +40,23 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" /> 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 <TextView
android:id="@+id/duration" android:id="@+id/duration"

View file

@ -166,6 +166,44 @@
android:orientation="vertical" /> android:orientation="vertical" />
</LinearLayout> </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> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
@ -198,6 +236,8 @@
</com.facebook.shimmer.ShimmerFrameLayout> </com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,26 +2,107 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:tools="http://schemas.android.com/tools">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/download_recyclerview" android:id="@+id/active_queued_linear"
android:orientation="vertical"
android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent" <LinearLayout
android:scrollbars="none" android:id="@+id/active_linear"
tools:listitem="@layout/active_download_card" android:layout_width="match_parent"
app:layout_constraintHorizontal_bias="0.0" android:layout_height="wrap_content"
android:paddingBottom="100dp" android:orientation="vertical">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume"> <include
</androidx.recyclerview.widget.RecyclerView> 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> </androidx.core.widget.NestedScrollView>

View file

@ -12,7 +12,7 @@
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/logs_toolbar" android:id="@+id/downloads_toolbar"
android:elevation="0dp" android:elevation="0dp"
app:title="@string/download_queue" app:title="@string/download_queue"
app:menu="@menu/download_queue_menu" 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:checked="false"
android:text="@string/get_new_uploads"/> 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 <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/retry_missing_downloads" android:id="@+id/retry_missing_downloads"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -25,6 +25,7 @@
android:id="@+id/video_view" android:id="@+id/video_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:resize_mode="fixed_height" app:resize_mode="fixed_height"
app:useDefaultControls="true" 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" <item android:title="@string/home"
android:icon="@drawable/ic_home" android:icon="@drawable/ic_home"
android:id="@+id/homeFragment"/> android:id="@+id/homeFragment"/>
<item android:title="@string/downloads" <item android:title="@string/downloads"
android:icon="@drawable/ic_downloads" android:icon="@drawable/ic_downloads"
android:id="@+id/historyFragment"/> android:id="@+id/historyFragment"/>
<item android:title="@string/more" <item android:title="@string/more"
android:icon="@drawable/ic_more" android:icon="@drawable/ic_more"
android:id="@+id/moreFragment"/> 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> </menu>

View file

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

View file

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

View file

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

View file

@ -384,8 +384,7 @@
<string name="minute">Menit</string> <string name="minute">Menit</string>
<string name="second">Detik</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_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">Urutan Prioritas Format</string>
<string name="format_importance_video">Urutan Prioritas Format (Video)</string>
<string name="no_audio">Video Tanpa Audio</string> <string name="no_audio">Video Tanpa Audio</string>
<string name="download_archive_folder">Folder Arsip Unduhan</string> <string name="download_archive_folder">Folder Arsip Unduhan</string>
<string name="location">Lokasi</string> <string name="location">Lokasi</string>

View file

@ -397,4 +397,5 @@
<string name="location">Vendi</string> <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="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="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> </resources>

View file

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

View file

@ -1463,4 +1463,16 @@
<item>container</item> <item>container</item>
</string-array> </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> </resources>

View file

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

View file

@ -401,4 +401,11 @@
<string name="location">Location</string> <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="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="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> </resources>

View file

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

View file

@ -69,7 +69,7 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/misc"> <PreferenceCategory android:title="YouTube">
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true" app:defaultValue="true"
@ -84,7 +84,24 @@
android:summary="@string/api_key_summary" android:summary="@string/api_key_summary"
app:title="@string/api_key" /> 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 <ListPreference
android:defaultValue="" android:defaultValue=""
android:entries="@array/countries" android:entries="@array/countries"
@ -95,15 +112,29 @@
app:key="locale" app:key="locale"
app:title="@string/preferred_locale" /> app:title="@string/preferred_locale" />
<ListPreference <Preference
android:defaultValue="Home" android:defaultValue="Home"
android:entries="@array/start_destination"
android:entryValues="@array/start_destination_values"
app:icon="@drawable/baseline_add_to_home_screen_24" app:icon="@drawable/baseline_add_to_home_screen_24"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:key="start_destination" app:key="start_destination"
app:isPreferenceVisible="false"
app:title="@string/preferred_home_screen" /> 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 <ListPreference
android:defaultValue="ytsearch" android:defaultValue="ytsearch"
android:entries="@array/search_engines" android:entries="@array/search_engines"
@ -157,4 +188,5 @@
app:key="ignore_battery" app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" /> app:title="@string/ignore_battery_optimization" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

@ -33,15 +33,9 @@
android:key="auto_update_ytdlp" android:key="auto_update_ytdlp"
app:title="@string/auto_update_ytdlp" /> app:title="@string/auto_update_ytdlp" />
<ListPreference </PreferenceCategory>
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 android:title="@string/format">
<ListPreference <ListPreference
android:defaultValue="filesize" android:defaultValue="filesize"
android:entries="@array/format_ordering" android:entries="@array/format_ordering"
@ -67,14 +61,6 @@
android:key="update_formats" android:key="update_formats"
app:summary="@string/update_formats_summary" app:summary="@string/update_formats_summary"
app:title="@string/update_formats" /> 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>
<PreferenceCategory android:title="@string/app"> <PreferenceCategory android:title="@string/app">

View file

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