way too many stuff
- long press download button to save it for later and not schedule it. Also works when u try to queue multiple items - change app icon background based on theme - fixed bug when trying to redownload a history item and wanting to use a different type - made the scheduled item auto-update after its queued so the app doesnt have to update when it begins downloading - some fixes with output templates - fixed container and vcodec being saved with translated strings and not constants - added download thumbnail functionality. Click the result card in the middle and use the video player. Also observe running and queued downloads for that item - removed api key - fixed recommendations sometimes not showing in that location - added feature to disable thumbnails on certain screens. U can choose - added feature to convert subs to different formats - added youtube music search provider - made app name have a color depending on the theme - fixed format card not showing a translated string on best and worst strings - added ability to hide the format intent filter from the share menu - fixed app killing active downloads when removing a queued item - fixed filename template not showing uploader on odysee - other small stuff
This commit is contained in:
parent
f303a4f879
commit
41306565ab
61 changed files with 2975 additions and 385 deletions
|
|
@ -10,7 +10,7 @@ def properties = new Properties()
|
|||
def versionMajor = 1
|
||||
def versionMinor = 6
|
||||
def versionPatch = 3
|
||||
def versionBuild = 4 // bump for dogfood builds, public betas, etc.
|
||||
def versionBuild = 5 // bump for dogfood builds, public betas, etc.
|
||||
def versionExt = ""
|
||||
|
||||
if (versionBuild > 0){
|
||||
|
|
|
|||
517
app/schemas/com.deniscerri.ytdlnis.database.DBManager/9.json
Normal file
517
app/schemas/com.deniscerri.ytdlnis.database.DBManager/9.json
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "bcd20f64b54fbc6e470be0d15059a66e",
|
||||
"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, `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": "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)",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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, `workID` BLOB)",
|
||||
"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": "workID",
|
||||
"columnName": "workID",
|
||||
"affinity": "BLOB",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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": []
|
||||
}
|
||||
],
|
||||
"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, 'bcd20f64b54fbc6e470be0d15059a66e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,13 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
|
@ -31,7 +38,8 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/BaseTheme"
|
||||
tools:ignore="DataExtractionRules"
|
||||
tools:targetApi="tiramisu">
|
||||
tools:targetApi="tiramisu"
|
||||
android:banner="@mipmap/ic_launcher">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="29" />
|
||||
|
|
@ -101,14 +109,71 @@
|
|||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".terminalShareAlias"
|
||||
android:targetActivity=".ui.more.TerminalActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".Default"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
|
||||
<activity-alias
|
||||
android:name=".DarkIcon"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_dark"
|
||||
android:roundIcon="@mipmap/ic_launcher_round_dark"
|
||||
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".LightIcon"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_light"
|
||||
android:roundIcon="@mipmap/ic_launcher_round_light"
|
||||
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".ui.more.WebViewActivity"
|
||||
|
|
|
|||
|
|
@ -5,9 +5,17 @@ import android.widget.Toast
|
|||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.deniscerri.ytdlnis.work.UpdateYTDLWorker
|
||||
import com.google.android.gms.common.internal.Constants
|
||||
import com.yausername.aria2c.Aria2c
|
||||
import com.yausername.ffmpeg.FFmpeg
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
|
|
@ -18,6 +26,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class App : Application() {
|
||||
|
|
@ -39,6 +48,23 @@ class App : Application() {
|
|||
putString("version", BuildConfig.VERSION_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
//init yt-dlp auto update with work request
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
|
||||
val periodicWorkRequest = PeriodicWorkRequestBuilder<UpdateYTDLWorker>(1, TimeUnit.DAYS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(this@App).enqueueUniquePeriodicWork(
|
||||
"ytdlp-Updater",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
periodicWorkRequest
|
||||
)
|
||||
|
||||
}catch (e: Exception){
|
||||
Toast.makeText(this@App, e.message, Toast.LENGTH_SHORT).show()
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ import androidx.fragment.app.FragmentContainerView
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.navOptions
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.deniscerri.ytdlnis.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
|
@ -9,6 +10,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
|
@ -29,10 +31,12 @@ import java.lang.StringBuilder
|
|||
class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
@ -52,20 +56,25 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
// THUMBNAIL ----------------------------------
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.image_view)
|
||||
val imageURL = item!!.thumb
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
|
||||
// PROGRESS BAR ----------------------------------------------------
|
||||
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
|
||||
progressBar.tag = "${item.id}##progress"
|
||||
progressBar.tag = "${item!!.id}##progress"
|
||||
progressBar.progress = 0
|
||||
progressBar.isIndeterminate = true
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
package com.deniscerri.ytdlnis.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.squareup.picasso.Picasso
|
||||
import java.lang.StringBuilder
|
||||
|
||||
class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadMinifiedAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
val cardView: MaterialCardView
|
||||
|
||||
init {
|
||||
cardView = itemView.findViewById(R.id.active_download_card_view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val cardView = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.active_download_card_minified, parent, false)
|
||||
return ViewHolder(cardView, onItemClickListener)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
|
||||
// PROGRESS BAR ----------------------------------------------------
|
||||
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
|
||||
progressBar.tag = "${item!!.id}##progress"
|
||||
progressBar.progress = 0
|
||||
progressBar.isIndeterminate = true
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.title)
|
||||
var title = item.title
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 40) + "..."
|
||||
}
|
||||
itemTitle.text = title.ifEmpty { item.url }
|
||||
|
||||
val duration = card.findViewById<TextView>(R.id.duration)
|
||||
duration.text = item.duration
|
||||
|
||||
|
||||
val formatDetailsChip = card.findViewById<TextView>(R.id.format_note)
|
||||
val formatDetailsText = StringBuilder(item.format.format_note.uppercase())
|
||||
|
||||
val codecText =
|
||||
if (item.format.encoding != "") {
|
||||
item.format.encoding.uppercase()
|
||||
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
|
||||
item.format.vcodec.uppercase()
|
||||
} else {
|
||||
item.format.acodec.uppercase()
|
||||
}
|
||||
if (codecText != "" && codecText != "none"){
|
||||
formatDetailsText.append(" \t •\t $codecText")
|
||||
}
|
||||
|
||||
val fileSize = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSize != "?") formatDetailsText.append(" \t •\t $fileSize")
|
||||
|
||||
formatDetailsChip.text = formatDetailsText
|
||||
|
||||
// CANCEL BUTTON ----------------------------------
|
||||
val cancelButton = card.findViewById<MaterialButton>(R.id.active_download_cancel)
|
||||
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
|
||||
|
||||
cancelButton.setOnClickListener {
|
||||
onItemClickListener.onCancelClick(item.id)
|
||||
}
|
||||
|
||||
card.setOnClickListener {
|
||||
onItemClickListener.onCardClick()
|
||||
}
|
||||
}
|
||||
interface OnItemClickListener {
|
||||
fun onCancelClick(itemID: Long)
|
||||
fun onCardClick()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() {
|
||||
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
val ranged = arrayListOf(oldItem.id, newItem.id)
|
||||
return ranged[0] == ranged[1]
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.deniscerri.ytdlnis.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -9,6 +11,7 @@ import android.view.ViewGroup
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
|
@ -24,10 +27,12 @@ import java.util.Locale
|
|||
class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ConfigureMultipleDownloadsAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences : SharedPreferences
|
||||
|
||||
init {
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
@ -47,18 +52,25 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
// THUMBNAIL ----------------------------------
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
val imageURL = item!!.thumb
|
||||
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
|
||||
val duration = card.findViewById<TextView>(R.id.duration)
|
||||
duration.text = item.duration
|
||||
duration.text = item!!.duration
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.title)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.deniscerri.ytdlnis.adapter
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -9,6 +11,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
|
@ -25,11 +28,13 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
|
|||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val checkedItems: ArrayList<Long>
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
checkedItems = ArrayList()
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
@ -49,18 +54,23 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
|
|||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
val imageURL = item!!.thumb
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
|
||||
val duration = card.findViewById<TextView>(R.id.duration)
|
||||
duration.text = item.duration
|
||||
duration.text = item!!.duration
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.title)
|
||||
|
|
@ -102,6 +112,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
|
|||
|
||||
when(item.status){
|
||||
DownloadRepository.Status.Cancelled.toString() -> actionButton.setIconResource(R.drawable.ic_refresh)
|
||||
DownloadRepository.Status.Saved.toString() -> actionButton.setIconResource(R.drawable.ic_downloads)
|
||||
DownloadRepository.Status.Queued.toString() -> actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24)
|
||||
else -> {
|
||||
actionButton.setIconResource(R.drawable.ic_baseline_file_open_24)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.adapter
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
|
|
@ -13,6 +14,7 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
|
@ -32,11 +34,13 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
|
|||
private val checkedItems: ArrayList<Long>
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
checkedItems = ArrayList()
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
@ -56,20 +60,26 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
// THUMBNAIL ----------------------------------
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
val imageURL = item!!.thumb
|
||||
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("downloads")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.downloads_title)
|
||||
var title = item.title
|
||||
var title = item!!.title
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 40) + "..."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.deniscerri.ytdlnis.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
|
@ -10,6 +11,8 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
|
@ -26,11 +29,13 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
|
|||
private val checkedVideos: ArrayList<String>
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
checkedVideos = ArrayList()
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
@ -50,22 +55,26 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val video = getItem(position)
|
||||
val card = holder.cardView
|
||||
// THUMBNAIL ----------------------------------
|
||||
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.result_image_view)
|
||||
val imageURL = video!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
|
||||
val imageURL = video!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
} else {
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
}
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val videoTitle = card.findViewById<TextView>(R.id.result_title)
|
||||
var title = video.title
|
||||
var title = video!!.title
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 40) + "..."
|
||||
}
|
||||
|
|
@ -145,6 +154,8 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
|
|||
card.setOnClickListener {
|
||||
if (checkedVideos.size > 0) {
|
||||
checkCard(card, videoURL)
|
||||
}else{
|
||||
onItemClickListener.onCardDetailsClick(videoURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,6 +176,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
|
|||
fun onButtonClick(videoURL: String, type: DownloadViewModel.Type?)
|
||||
fun onLongButtonClick(videoURL: String, type: DownloadViewModel.Type?)
|
||||
fun onCardClick(videoURL: String, add: Boolean)
|
||||
fun onCardDetailsClick(videoURL: String)
|
||||
}
|
||||
|
||||
fun checkAll(items: List<ResultItem?>?){
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.adapter
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
|
@ -12,6 +13,7 @@ import android.widget.CheckBox
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
|
@ -26,11 +28,13 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
|
|||
private val checkedItems: ArrayList<String>
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
checkedItems = ArrayList()
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
@ -50,20 +54,25 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
// THUMBNAIL ----------------------------------
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
val imageURL = item!!.thumb
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(95, 0, 0, 0))
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(95, 0, 0, 0))
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.title)
|
||||
var title = item.title
|
||||
var title = item!!.title
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 40) + "..."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ interface DownloadDao {
|
|||
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
|
||||
fun getErroredDownloadsList() : List<DownloadItem>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
|
||||
fun getProcessingDownloads() : Flow<List<DownloadItem>>
|
||||
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
|
||||
fun getSavedDownloads() : Flow<List<DownloadItem>>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
|
||||
fun getDownloadById(id: Long) : DownloadItem
|
||||
|
|
@ -70,6 +70,9 @@ interface DownloadDao {
|
|||
@Query("DELETE FROM downloads WHERE status='Error'")
|
||||
suspend fun deleteErrored()
|
||||
|
||||
@Query("DELETE FROM downloads WHERE status='Saved'")
|
||||
suspend fun deleteSaved()
|
||||
|
||||
@Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued'")
|
||||
suspend fun cancelQueued()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
val queuedDownloads : Flow<List<DownloadItem>> = downloadDao.getQueuedDownloads()
|
||||
val cancelledDownloads : Flow<List<DownloadItem>> = downloadDao.getCancelledDownloads()
|
||||
val erroredDownloads : Flow<List<DownloadItem>> = downloadDao.getErroredDownloads()
|
||||
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads()
|
||||
val savedDownloads : Flow<List<DownloadItem>> = downloadDao.getSavedDownloads()
|
||||
|
||||
enum class Status {
|
||||
Active, Queued, Error, Processing, Cancelled
|
||||
Active, Queued, Error, Processing, Cancelled, Saved
|
||||
}
|
||||
|
||||
suspend fun insert(item: DownloadItem) : Long {
|
||||
|
|
@ -73,6 +73,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
downloadDao.deleteErrored()
|
||||
}
|
||||
|
||||
suspend fun deleteSaved(){
|
||||
downloadDao.deleteSaved()
|
||||
}
|
||||
|
||||
suspend fun cancelQueued(){
|
||||
downloadDao.cancelQueued()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
suspend fun updateTrending(){
|
||||
deleteAll()
|
||||
val infoUtil = InfoUtil(context)
|
||||
val items = infoUtil.getTrending(context)
|
||||
val items = infoUtil.getTrending()
|
||||
itemCount.value = items.size
|
||||
for (i in items){
|
||||
resultDao.insert(i!!)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@ import androidx.work.ExistingWorkPolicy
|
|||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import com.deniscerri.ytdlnis.App
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
|
||||
import com.deniscerri.ytdlnis.database.dao.DownloadDao
|
||||
import com.deniscerri.ytdlnis.database.dao.ResultDao
|
||||
import com.deniscerri.ytdlnis.database.models.AudioPreferences
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
|
|
@ -40,13 +43,14 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class DownloadViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val dbManager: DBManager
|
||||
private val repository : DownloadRepository
|
||||
private val sharedPreferences: SharedPreferences
|
||||
private val commandTemplateDao: CommandTemplateDao
|
||||
|
|
@ -57,7 +61,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
val activeDownloadsCount : LiveData<Int>
|
||||
val cancelledDownloads : LiveData<List<DownloadItem>>
|
||||
val erroredDownloads : LiveData<List<DownloadItem>>
|
||||
val processingDownloads : LiveData<List<DownloadItem>>
|
||||
val savedDownloads : LiveData<List<DownloadItem>>
|
||||
|
||||
private var bestVideoFormat : Format
|
||||
private var bestAudioFormat : Format
|
||||
|
|
@ -72,7 +76,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
}
|
||||
|
||||
init {
|
||||
val dao = DBManager.getInstance(application).downloadDao
|
||||
dbManager = DBManager.getInstance(application)
|
||||
val dao = dbManager.downloadDao
|
||||
repository = DownloadRepository(dao)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
|
||||
|
|
@ -82,7 +87,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
queuedDownloads = repository.queuedDownloads.asLiveData()
|
||||
activeDownloads = repository.activeDownloads.asLiveData()
|
||||
activeDownloadsCount = repository.activeDownloadsCount.asLiveData()
|
||||
processingDownloads = repository.processingDownloads.asLiveData()
|
||||
savedDownloads = repository.savedDownloads.asLiveData()
|
||||
cancelledDownloads = repository.cancelledDownloads.asLiveData()
|
||||
erroredDownloads = repository.erroredDownloads.asLiveData()
|
||||
|
||||
|
|
@ -97,8 +102,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
|
||||
|
||||
val videoFormatValues = resources.getStringArray(R.array.video_formats_values)
|
||||
var videoContainer = sharedPreferences.getString("video_format", "Default")
|
||||
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
|
||||
val videoContainer = sharedPreferences.getString("video_format", "Default")
|
||||
|
||||
defaultVideoFormats = mutableListOf()
|
||||
videoFormatValues.forEach {
|
||||
|
|
@ -116,8 +120,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
|
||||
bestVideoFormat = defaultVideoFormats.last()
|
||||
|
||||
var audioContainer = sharedPreferences.getString("audio_format", "mp3")
|
||||
if (audioContainer == "Default") audioContainer = App.instance.getString(R.string.defaultValue)
|
||||
val audioContainer = sharedPreferences.getString("audio_format", "mp3")
|
||||
bestAudioFormat = Format(
|
||||
"best",
|
||||
audioContainer!!,
|
||||
|
|
@ -374,8 +377,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
fun getGenericAudioFormats() : MutableList<Format>{
|
||||
val audioFormats = resources.getStringArray(R.array.audio_formats)
|
||||
val formats = mutableListOf<Format>()
|
||||
var containerPreference = sharedPreferences.getString("audio_format", "Default")
|
||||
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)
|
||||
val containerPreference = sharedPreferences.getString("audio_format", "")
|
||||
audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) }
|
||||
return formats
|
||||
}
|
||||
|
|
@ -383,9 +385,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
fun getGenericVideoFormats() : MutableList<Format>{
|
||||
val videoFormats = resources.getStringArray(R.array.video_formats_values)
|
||||
val formats = mutableListOf<Format>()
|
||||
var containerPreference = sharedPreferences.getString("video_format", "Default")
|
||||
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)
|
||||
videoFormats.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.defaultValue),"", "",0, it)) }
|
||||
val containerPreference = sharedPreferences.getString("video_format", "")
|
||||
videoFormats.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) }
|
||||
return formats
|
||||
}
|
||||
|
||||
|
|
@ -415,6 +416,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
repository.deleteErrored()
|
||||
}
|
||||
|
||||
fun deleteSaved() = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.deleteSaved()
|
||||
}
|
||||
|
||||
fun cancelQueued() = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.cancelQueued()
|
||||
}
|
||||
|
|
@ -450,6 +455,9 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
|
||||
val queuedItems = mutableListOf<DownloadItem>()
|
||||
var lastDownloadId = repository.getLastDownloadId()
|
||||
|
||||
var exists = false
|
||||
|
||||
items.forEach {
|
||||
lastDownloadId++
|
||||
it.status = DownloadRepository.Status.Queued.toString()
|
||||
|
|
@ -458,9 +466,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
d.status = DownloadRepository.Status.Queued.toString()
|
||||
d.toString() == it.toString()
|
||||
} != null) {
|
||||
Looper.prepare().run {
|
||||
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
exists = true
|
||||
}else{
|
||||
if (it.id == 0L){
|
||||
it.id = lastDownloadId
|
||||
|
|
@ -475,6 +481,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
}
|
||||
}
|
||||
|
||||
if (exists){
|
||||
Looper.prepare().run {
|
||||
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
queuedItems.forEach {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
var delay = if (it.downloadStartTime != 0L){
|
||||
|
|
@ -498,12 +510,6 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
workRequest
|
||||
).enqueue()
|
||||
|
||||
it.id = withContext(Dispatchers.IO){
|
||||
repository.checkIfReDownloadingErroredOrCancelled(it)
|
||||
}
|
||||
if (it.id != 0L){
|
||||
repository.delete(it)
|
||||
}
|
||||
}
|
||||
|
||||
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
|
||||
|
|
@ -512,6 +518,35 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
items.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
|
||||
try{
|
||||
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
|
||||
}catch (ignored: Exception){}
|
||||
}
|
||||
}
|
||||
private fun updateDownloadItem(
|
||||
downloadItem: DownloadItem,
|
||||
infoUtil: InfoUtil,
|
||||
dao: DownloadDao,
|
||||
resultDao: ResultDao
|
||||
) : Boolean {
|
||||
var wasQuickDownloaded = false
|
||||
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
|
||||
runCatching {
|
||||
val info = infoUtil.getMissingInfo(downloadItem.url)
|
||||
if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString()
|
||||
if (downloadItem.author.isEmpty()) downloadItem.author = info?.author.toString()
|
||||
downloadItem.duration = info?.duration.toString()
|
||||
downloadItem.website = info?.website.toString()
|
||||
if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info?.thumb.toString()
|
||||
runBlocking {
|
||||
wasQuickDownloaded = resultDao.getCountInt() == 0
|
||||
dao.update(downloadItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return wasQuickDownloaded
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import android.app.Activity
|
|||
import android.content.*
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources.Theme
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.View.*
|
||||
import android.widget.*
|
||||
|
|
@ -37,7 +39,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
|||
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.ThemeUtil
|
||||
import com.facebook.shimmer.ShimmerFrameLayout
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
|
|
@ -133,6 +137,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
searchHistory = view.findViewById(R.id.search_history_scroll_view)
|
||||
searchHistoryLinearLayout = view.findViewById(R.id.search_history_linear_layout)
|
||||
|
||||
materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext())
|
||||
|
||||
homeAdapter =
|
||||
HomeAdapter(
|
||||
this,
|
||||
|
|
@ -567,6 +573,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
|
||||
}
|
||||
|
||||
override fun onCardDetailsClick(videoURL: String) {
|
||||
if (parentFragmentManager.findFragmentByTag("resultDetails") == null && resultsList != null && resultsList!!.isNotEmpty()){
|
||||
val bottomSheet = ResultCardDetailsDialog(resultsList!!.first{it!!.url == videoURL}!!)
|
||||
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val viewIdName: String = try {
|
||||
v.tag.toString()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.deniscerri.ytdlnis.R
|
|||
import com.deniscerri.ytdlnis.database.models.ChapterItem
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
|
@ -104,64 +105,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
behavior.peekHeight = displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
// val renderersFactory = DefaultRenderersFactory(requireContext().applicationContext)
|
||||
// .setEnableDecoderFallback(true)
|
||||
// .setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
|
||||
// .setMediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
|
||||
// var decoderInfo: MutableList<MediaCodecInfo> =
|
||||
// MediaCodecSelector.DEFAULT
|
||||
// .getDecoderInfos(
|
||||
// mimeType,
|
||||
// requiresSecureDecoder,
|
||||
// requiresTunnelingDecoder
|
||||
// )
|
||||
// if (MimeTypes.VIDEO_H264 == mimeType) {
|
||||
// // copy the list because MediaCodecSelector.DEFAULT returns an unmodifiable list
|
||||
// decoderInfo = ArrayList(decoderInfo)
|
||||
// decoderInfo.reverse()
|
||||
// }
|
||||
// decoderInfo
|
||||
// }
|
||||
|
||||
val cronetEngine: CronetEngine = CronetEngine.Builder(App.instance)
|
||||
.enableHttp2(true)
|
||||
.enableQuic(true)
|
||||
.enableBrotli(true)
|
||||
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 1024L * 1024L) // 1MiB
|
||||
.build()
|
||||
|
||||
val trackSelector = DefaultTrackSelector(requireContext())
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
// cache the last three minutes
|
||||
.setBackBuffer(1000 * 60 * 3, true)
|
||||
.setBufferDurationsMs(
|
||||
1000 * 10, // exo default is 50s
|
||||
50000,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
|
||||
val cronetDataSourceFactory = CronetDataSource.Factory(
|
||||
cronetEngine,
|
||||
Executors.newCachedThreadPool()
|
||||
)
|
||||
val dataSourceFactory = DefaultDataSource.Factory(requireContext(), cronetDataSourceFactory)
|
||||
|
||||
player = ExoPlayer.Builder(requireContext())
|
||||
.setUsePlatformDiagnostics(false)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
,
|
||||
true)
|
||||
.build()
|
||||
player = VideoPlayerUtil.buildPlayer(requireContext())
|
||||
|
||||
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
|
||||
val videoView = view.findViewById<PlayerView>(R.id.video_view)
|
||||
|
|
|
|||
|
|
@ -142,9 +142,13 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
|
|||
//if its updating a present downloaditem and its the wrong category
|
||||
if (currentDownloadItem!!.type != Type.audio){
|
||||
downloadItem.type = Type.audio
|
||||
downloadItem.format =
|
||||
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
|
||||
.maxByOrNull { it.filesize }!!
|
||||
runCatching {
|
||||
downloadItem.format =
|
||||
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
|
||||
.maxByOrNull { it.filesize }!!
|
||||
}.onFailure {
|
||||
downloadItem.format = downloadViewModel.getGenericAudioFormats().last()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) })
|
||||
|
|
@ -160,11 +164,11 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
|
|||
|
||||
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
|
||||
val chosenFormat = downloadItem.format
|
||||
UiUtil.populateFormatCard(formatCard, chosenFormat, null)
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
|
||||
val listener = object : OnFormatClickListener {
|
||||
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) {
|
||||
downloadItem.format = item.first().format
|
||||
UiUtil.populateFormatCard(formatCard, item.first().format, null)
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null)
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
resultItem.formats.removeAll(formats.toSet())
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.deniscerri.ytdlnis.database.DBManager
|
|||
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
||||
|
|
@ -33,6 +34,7 @@ import com.deniscerri.ytdlnis.util.UiUtil
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kotlinx.coroutines.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
|
@ -201,6 +203,22 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
|
|||
dismiss()
|
||||
}
|
||||
|
||||
download.setOnLongClickListener {
|
||||
val dd = MaterialAlertDialogBuilder(requireContext())
|
||||
dd.setTitle(getString(R.string.save_for_later))
|
||||
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
lifecycleScope.launch(Dispatchers.IO){
|
||||
val item = getDownloadItem()
|
||||
item.status = DownloadRepository.Status.Saved.toString()
|
||||
downloadViewModel.updateDownload(item)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
dd.show()
|
||||
true
|
||||
}
|
||||
|
||||
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
link.text = resultItem.url
|
||||
link.setOnClickListener{
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
|
|||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.Format
|
||||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
||||
|
|
@ -139,6 +140,23 @@ class DownloadMultipleBottomSheetDialog(private var results: List<ResultItem?>,
|
|||
dismiss()
|
||||
}
|
||||
|
||||
download.setOnLongClickListener {
|
||||
val dd = MaterialAlertDialogBuilder(requireContext())
|
||||
dd.setTitle(getString(R.string.save_for_later))
|
||||
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
lifecycleScope.launch(Dispatchers.IO){
|
||||
items.forEach {
|
||||
it.status = DownloadRepository.Status.Saved.toString()
|
||||
downloadViewModel.updateDownload(it)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
dd.show()
|
||||
true
|
||||
}
|
||||
|
||||
bottomAppBar = view.findViewById(R.id.bottomAppBar)
|
||||
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
|
||||
when(items.first().type){
|
||||
|
|
|
|||
|
|
@ -147,9 +147,13 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
|
|||
//if its updating a present downloaditem and its the wrong category
|
||||
if (currentDownloadItem!!.type != Type.video){
|
||||
downloadItem.type = Type.video
|
||||
downloadItem.format =
|
||||
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
|
||||
.maxByOrNull { it.filesize }!!
|
||||
runCatching {
|
||||
downloadItem.format =
|
||||
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
|
||||
.maxByOrNull { it.filesize }!!
|
||||
}.onFailure {
|
||||
downloadItem.format = downloadViewModel.getGenericVideoFormats().last()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats)
|
||||
|
|
@ -166,7 +170,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
|
|||
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
|
||||
|
||||
val chosenFormat = downloadItem.format
|
||||
UiUtil.populateFormatCard(formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
|
||||
val listener = object : OnFormatClickListener {
|
||||
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) {
|
||||
downloadItem.format = item.first().format
|
||||
|
|
@ -180,7 +184,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
|
|||
}
|
||||
}
|
||||
formats = allFormats.first().toMutableList()
|
||||
UiUtil.populateFormatCard(formatCard, item.first().format, item.first().audioFormats)
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats)
|
||||
}
|
||||
}
|
||||
formatCard.setOnClickListener{
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
|
|||
val format = finalFormats[i]
|
||||
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
|
||||
formatItem.tag = "${format.format_id}${format.format_note}"
|
||||
UiUtil.populateFormatCard(formatItem as MaterialCardView, format, null)
|
||||
UiUtil.populateFormatCard(requireContext(), formatItem as MaterialCardView, format, null)
|
||||
formatItem.setOnClickListener{ clickedformat ->
|
||||
//if the context is behind a video or playlist, allow the ability to multiselect audio formats
|
||||
if (canMultiSelectAudio){
|
||||
|
|
|
|||
|
|
@ -0,0 +1,560 @@
|
|||
package com.deniscerri.ytdlnis.ui.downloadcard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.format.DateFormat
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.map
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.ActiveDownloadMinifiedAdapter
|
||||
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener {
|
||||
private lateinit var infoUtil: InfoUtil
|
||||
private lateinit var notificationUtil: NotificationUtil
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var videoView: PlayerView
|
||||
private lateinit var downloadViewModel: DownloadViewModel
|
||||
private lateinit var resultViewModel: ResultViewModel
|
||||
|
||||
private lateinit var activeDownloads: ActiveDownloadMinifiedAdapter
|
||||
private lateinit var queuedDownloads: GenericDownloadAdapter
|
||||
|
||||
private lateinit var queuedItems : List<DownloadItem>
|
||||
private lateinit var activeItems : List<DownloadItem>
|
||||
private lateinit var downloadManager: DownloadManager
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
infoUtil = InfoUtil(requireActivity().applicationContext)
|
||||
notificationUtil = NotificationUtil(requireActivity().applicationContext)
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
|
||||
queuedItems = listOf()
|
||||
activeItems = listOf()
|
||||
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
// Inflate the layout to use as dialog or embedded fragment
|
||||
return inflater.inflate(R.layout.result_card_details, container, false)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
return dialog
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi", "SetTextI18n", "UseGetLayoutInflater")
|
||||
override fun setupDialog(dialog: Dialog, style: Int) {
|
||||
super.setupDialog(dialog, style)
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null)
|
||||
dialog.setContentView(view)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activeDownloads = ActiveDownloadMinifiedAdapter(this,requireActivity())
|
||||
queuedDownloads = GenericDownloadAdapter(this,requireActivity())
|
||||
|
||||
val bottomSheetLink = view.findViewById<MaterialButton>(R.id.bottom_sheet_link)
|
||||
val downloadThumb = view.findViewById<MaterialButton>(R.id.download_thumb)
|
||||
val title = view.findViewById<TextView>(R.id.title)
|
||||
val bottomInfo = view.findViewById<TextView>(R.id.bottom_info)
|
||||
val downloadMusic = view.findViewById<Button>(R.id.download_music)
|
||||
val downloadVideo = view.findViewById<Button>(R.id.download_video)
|
||||
|
||||
val runningRecycler = view.findViewById<RecyclerView>(R.id.running_recycler)
|
||||
val running = view.findViewById<TextView>(R.id.running)
|
||||
val queuedRecycler = view.findViewById<RecyclerView>(R.id.queued_recycler)
|
||||
val queued = view.findViewById<TextView>(R.id.queued)
|
||||
|
||||
runningRecycler.adapter = activeDownloads
|
||||
runningRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
if (preferences.getBoolean("swipe_gestures", true)){
|
||||
val itemTouchHelper = ItemTouchHelper(simpleCallback)
|
||||
itemTouchHelper.attachToRecyclerView(queuedRecycler)
|
||||
}
|
||||
|
||||
queuedRecycler.adapter = queuedDownloads
|
||||
queuedRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
downloadViewModel.activeDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) {
|
||||
if (it.isEmpty()) {
|
||||
running.visibility = View.GONE
|
||||
runningRecycler.visibility = View.GONE
|
||||
}else{
|
||||
running.visibility = View.VISIBLE
|
||||
runningRecycler.visibility = View.VISIBLE
|
||||
|
||||
it.forEach{item ->
|
||||
WorkManager.getInstance(requireContext())
|
||||
.getWorkInfosForUniqueWorkLiveData(item.id.toString())
|
||||
.observe(viewLifecycleOwner){ list ->
|
||||
list.forEach {work ->
|
||||
if (work == null) return@observe
|
||||
val id = work.progress.getLong("id", 0L)
|
||||
if(id == 0L) return@observe
|
||||
|
||||
val progress = work.progress.getInt("progress", 0)
|
||||
|
||||
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
|
||||
|
||||
requireActivity().runOnUiThread {
|
||||
try {
|
||||
progressBar?.setProgressCompat(progress, true)
|
||||
}catch (ignored: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeItems = it
|
||||
activeDownloads.submitList(it)
|
||||
}
|
||||
|
||||
|
||||
downloadViewModel.queuedDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) {
|
||||
if (it.isEmpty()) {
|
||||
queued.visibility = View.GONE
|
||||
queuedRecycler.visibility = View.GONE
|
||||
}else{
|
||||
queued.visibility = View.VISIBLE
|
||||
queuedRecycler.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
queuedItems = it
|
||||
queuedDownloads.submitList(it)
|
||||
}
|
||||
|
||||
|
||||
bottomSheetLink.text = item.url
|
||||
bottomSheetLink.setOnClickListener{
|
||||
UiUtil.openLinkIntent(requireContext(), item.url, null)
|
||||
}
|
||||
bottomSheetLink.setOnLongClickListener{
|
||||
UiUtil.copyLinkToClipBoard(requireContext(), item.url, null)
|
||||
true
|
||||
}
|
||||
|
||||
downloadThumb.setOnClickListener {
|
||||
downloadManager.enqueue(
|
||||
DownloadManager.Request(Uri.parse(item.thumb))
|
||||
.setAllowedNetworkTypes(
|
||||
DownloadManager.Request.NETWORK_WIFI or
|
||||
DownloadManager.Request.NETWORK_MOBILE
|
||||
)
|
||||
.setAllowedOverRoaming(true)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setTitle(requireContext().getString(R.string.app_name))
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis"))
|
||||
}
|
||||
|
||||
title.text = item.title
|
||||
bottomInfo.text = item.author
|
||||
|
||||
downloadMusic.setOnClickListener {
|
||||
onButtonClick(DownloadViewModel.Type.audio)
|
||||
}
|
||||
|
||||
downloadVideo.setOnClickListener {
|
||||
onButtonClick(DownloadViewModel.Type.video)
|
||||
}
|
||||
|
||||
|
||||
videoView = view.findViewById<PlayerView>(R.id.video_view)
|
||||
val pause = view.findViewById<MaterialButton>(R.id.pause)
|
||||
val frameLayout = view.findViewById<FrameLayout>(R.id.frame_layout)
|
||||
|
||||
val player = VideoPlayerUtil.buildPlayer(requireContext())
|
||||
videoView.player = player
|
||||
|
||||
videoView.setOnClickListener {
|
||||
if (player.isPlaying){
|
||||
player.pause()
|
||||
pause.visibility = View.VISIBLE
|
||||
}
|
||||
else {
|
||||
player.play()
|
||||
pause.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val data : MutableList<String?> = withContext(Dispatchers.IO){
|
||||
if (item.urls.isEmpty()) {
|
||||
infoUtil.getStreamingUrlAndChapters(item.url)
|
||||
}else {
|
||||
item.urls.split("\n").toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
data.removeFirst()
|
||||
|
||||
if (data.isEmpty()) throw Exception("No Streaming URL found!")
|
||||
if (data.size == 2){
|
||||
val audioSource : MediaSource =
|
||||
DefaultMediaSourceFactory(requireContext())
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(data[0])))
|
||||
val videoSource: MediaSource =
|
||||
DefaultMediaSourceFactory(requireContext())
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(data[1])))
|
||||
player.setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
}else{
|
||||
player.addMediaItem(MediaItem.fromUri(Uri.parse(data[0])))
|
||||
}
|
||||
player.addMediaItem(MediaItem.fromUri(Uri.parse(data[0])))
|
||||
|
||||
player.prepare()
|
||||
player.play()
|
||||
}catch (e: Exception){
|
||||
frameLayout.visibility = View.GONE
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onButtonClick(type: DownloadViewModel.Type){
|
||||
this.dismiss()
|
||||
if (sharedPreferences.getBoolean("download_card", true)) {
|
||||
val bottomSheet = DownloadBottomSheetDialog(item, type, null, false)
|
||||
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
|
||||
} else {
|
||||
lifecycleScope.launch{
|
||||
val downloadItem = withContext(Dispatchers.IO){
|
||||
downloadViewModel.createDownloadItemFromResult(item, type)
|
||||
}
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
|
||||
private fun cleanUp(){
|
||||
kotlin.runCatching {
|
||||
videoView.player?.stop()
|
||||
videoView.player?.release()
|
||||
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("resultDetails")!!).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeQueuedItem(id: Long){
|
||||
val item = queuedItems.find { it.id == id } ?: return
|
||||
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()
|
||||
downloadViewModel.updateDownload(item)
|
||||
|
||||
Snackbar.make(requireView().rootView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
downloadViewModel.deleteDownload(item)
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
private var simpleCallback: ItemTouchHelper.SimpleCallback =
|
||||
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
when (direction) {
|
||||
ItemTouchHelper.LEFT -> {
|
||||
val deletedItem = queuedItems[position]
|
||||
queuedDownloads.notifyItemChanged(position)
|
||||
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 onActionButtonClick(itemID: Long) {
|
||||
removeQueuedItem(itemID)
|
||||
}
|
||||
|
||||
override fun onCardClick(itemID: Long) {
|
||||
val item = queuedItems.find { it.id == itemID } ?: return
|
||||
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
|
||||
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
|
||||
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
|
||||
when (item.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
|
||||
}
|
||||
else -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
|
||||
}
|
||||
}
|
||||
|
||||
val time = bottomSheet.findViewById<Chip>(R.id.time)
|
||||
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
|
||||
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
|
||||
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
|
||||
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
|
||||
|
||||
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
|
||||
else {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = item.downloadStartTime
|
||||
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
|
||||
|
||||
time.setOnClickListener {
|
||||
UiUtil.showDatePicker(parentFragmentManager) {
|
||||
bottomSheet.dismiss()
|
||||
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
|
||||
downloadViewModel.deleteDownload(item)
|
||||
item.downloadStartTime = it.timeInMillis
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(item.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
else formatNote!!.text = item.format.format_note
|
||||
|
||||
if (item.format.container != "") container!!.text = item.format.container.uppercase()
|
||||
else container!!.visibility = View.GONE
|
||||
|
||||
val codecText =
|
||||
if (item.format.encoding != "") {
|
||||
item.format.encoding.uppercase()
|
||||
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
|
||||
item.format.vcodec.uppercase()
|
||||
} else {
|
||||
item.format.acodec.uppercase()
|
||||
}
|
||||
if (codecText == "" || codecText == "none"){
|
||||
codec!!.visibility = View.GONE
|
||||
}else{
|
||||
codec!!.visibility = View.VISIBLE
|
||||
codec.text = codecText
|
||||
}
|
||||
|
||||
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
|
||||
else fileSize!!.text = fileSizeReadable
|
||||
|
||||
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
val url = item.url
|
||||
link!!.text = url
|
||||
link.tag = itemID
|
||||
link.setOnClickListener{
|
||||
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
|
||||
}
|
||||
link.setOnLongClickListener{
|
||||
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
|
||||
true
|
||||
}
|
||||
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
|
||||
remove!!.tag = itemID
|
||||
remove.setOnClickListener{
|
||||
bottomSheet.hide()
|
||||
removeQueuedItem(itemID)
|
||||
}
|
||||
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
|
||||
openFile!!.visibility = View.GONE
|
||||
|
||||
|
||||
|
||||
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
|
||||
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
|
||||
else{
|
||||
downloadNow!!.text = getString(R.string.download_now)
|
||||
downloadNow.setOnClickListener {
|
||||
bottomSheet.dismiss()
|
||||
downloadViewModel.deleteDownload(item)
|
||||
item.downloadStartTime = 0
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(item.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bottomSheet.show()
|
||||
val displayMetrics = DisplayMetrics()
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
bottomSheet.window!!.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onCancelClick(itemID: Long) {
|
||||
cancelActiveDownload(itemID)
|
||||
}
|
||||
|
||||
override fun onCardClick() {
|
||||
this.dismiss()
|
||||
findNavController().navigate(
|
||||
R.id.downloadQueueMainFragment
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelActiveDownload(itemID: Long){
|
||||
val id = itemID.toInt()
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id)
|
||||
|
||||
activeItems.find { it.id == itemID }?.let {
|
||||
it.status = DownloadRepository.Status.Cancelled.toString()
|
||||
downloadViewModel.updateDownload(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
overScrollMode = View.OVER_SCROLL_NEVER
|
||||
}
|
||||
|
||||
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment())
|
||||
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
|
||||
|
||||
fragmentAdapter = DownloadListFragmentAdapter(
|
||||
childFragmentManager,
|
||||
|
|
@ -80,6 +80,7 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
1 -> tab.text = getString(R.string.in_queue)
|
||||
2 -> tab.text = getString(R.string.cancelled)
|
||||
3 -> tab.text = getString(R.string.errored)
|
||||
4 -> tab.text = getString(R.string.saved)
|
||||
}
|
||||
}.attach()
|
||||
|
||||
|
|
@ -113,21 +114,26 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
try{
|
||||
when(m.itemId){
|
||||
R.id.clear_queue -> {
|
||||
showDeleteDialog() {
|
||||
showDeleteDialog {
|
||||
cancelAllDownloads()
|
||||
downloadViewModel.cancelQueued()
|
||||
}
|
||||
}
|
||||
R.id.clear_cancelled -> {
|
||||
showDeleteDialog() {
|
||||
showDeleteDialog {
|
||||
downloadViewModel.deleteCancelled()
|
||||
}
|
||||
}
|
||||
R.id.clear_errored -> {
|
||||
showDeleteDialog() {
|
||||
showDeleteDialog {
|
||||
downloadViewModel.deleteErrored()
|
||||
}
|
||||
}
|
||||
R.id.clear_saved -> {
|
||||
showDeleteDialog {
|
||||
downloadViewModel.deleteSaved()
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch (e: Exception){
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
private var _binding : FragmentHistoryBinding? = null
|
||||
private var actionMode : ActionMode? = null
|
||||
|
||||
private lateinit var sortChip: Chip
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
|
|
@ -111,6 +113,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
selectionChips = view.findViewById(R.id.history_selection_chips)
|
||||
websiteGroup = view.findViewById(R.id.website_chip_group)
|
||||
uiHandler = Handler(Looper.getMainLooper())
|
||||
sortChip = view.findViewById(R.id.sortChip)
|
||||
selectedObjects = ArrayList()
|
||||
|
||||
|
||||
|
|
@ -155,6 +158,27 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
scrollToTop()
|
||||
}
|
||||
|
||||
historyViewModel.sortOrder.observe(viewLifecycleOwner){
|
||||
if (it != null){
|
||||
when(it){
|
||||
HistorySort.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
|
||||
HistorySort.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
historyViewModel.sortType.observe(viewLifecycleOwner){
|
||||
if(it != null){
|
||||
when(it){
|
||||
HistoryRepository.HistorySortType.AUTHOR -> sortChip.text = getString(R.string.author)
|
||||
HistoryRepository.HistorySortType.DATE -> sortChip.text = getString(R.string.date_added)
|
||||
HistoryRepository.HistorySortType.TITLE -> sortChip.text = getString(R.string.title)
|
||||
HistoryRepository.HistorySortType.FILESIZE -> sortChip.text = getString(R.string.file_size)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
initMenu()
|
||||
initChips()
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import kotlinx.coroutines.runBlocking
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
|
||||
|
|
@ -102,9 +103,13 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
|
|||
items = it.toMutableList()
|
||||
if (it.isEmpty()) fileSize.visibility = View.GONE
|
||||
else{
|
||||
fileSize.visibility = View.VISIBLE
|
||||
fileSize.text = "${getString(R.string.file_size)}: ~ ${FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize })}"
|
||||
}
|
||||
val size = FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize })
|
||||
if (size == "?") fileSize.visibility = View.GONE
|
||||
else {
|
||||
fileSize.visibility = View.VISIBLE
|
||||
fileSize.text = "${getString(R.string.file_size)}: ~ $size"
|
||||
}
|
||||
}
|
||||
queuedDownloads.submitList(it)
|
||||
}
|
||||
}
|
||||
|
|
@ -262,17 +267,13 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
|
|||
}
|
||||
}
|
||||
|
||||
private fun removeItem(itemID: Long){
|
||||
val item = items.find { it.id == itemID }
|
||||
private fun removeItem(id: Long){
|
||||
val item = items.find { it.id == id } ?: return
|
||||
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item?.title + "\"!")
|
||||
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 ->
|
||||
val id = itemID.toInt()
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id)
|
||||
item!!.status = DownloadRepository.Status.Cancelled.toString()
|
||||
item.status = DownloadRepository.Status.Cancelled.toString()
|
||||
downloadViewModel.updateDownload(item)
|
||||
|
||||
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,385 @@
|
|||
package com.deniscerri.ytdlnis.ui.downloads
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
|
||||
private var fragmentView: View? = null
|
||||
private var activity: Activity? = null
|
||||
private lateinit var downloadViewModel : DownloadViewModel
|
||||
private lateinit var savedRecyclerView: RecyclerView
|
||||
private lateinit var savedDownloads: GenericDownloadAdapter
|
||||
private var selectedObjects: ArrayList<DownloadItem>? = null
|
||||
private var actionMode : ActionMode? = null
|
||||
private lateinit var items : MutableList<DownloadItem>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
|
||||
activity = getActivity()
|
||||
selectedObjects = arrayListOf()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
items = mutableListOf()
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
savedDownloads =
|
||||
GenericDownloadAdapter(
|
||||
this,
|
||||
requireActivity()
|
||||
)
|
||||
|
||||
savedRecyclerView = view.findViewById(R.id.download_recyclerview)
|
||||
savedRecyclerView.adapter = savedDownloads
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
if (preferences.getBoolean("swipe_gestures", true)){
|
||||
val itemTouchHelper = ItemTouchHelper(simpleCallback)
|
||||
itemTouchHelper.attachToRecyclerView(savedRecyclerView)
|
||||
}
|
||||
savedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
|
||||
downloadViewModel.savedDownloads.observe(viewLifecycleOwner) {
|
||||
items = it.toMutableList()
|
||||
savedDownloads.submitList(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionButtonClick(itemID: Long) {
|
||||
val item = items.find { it.id == itemID }
|
||||
if (item != null){
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}else{
|
||||
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCardClick(itemID: Long) {
|
||||
val item = items.find { it.id == itemID } ?: return
|
||||
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
|
||||
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
|
||||
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
|
||||
when (item.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
|
||||
}
|
||||
else -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
|
||||
}
|
||||
}
|
||||
|
||||
val time = bottomSheet.findViewById<Chip>(R.id.time)
|
||||
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
|
||||
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
|
||||
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
|
||||
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
|
||||
|
||||
time!!.visibility = View.GONE
|
||||
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
else formatNote!!.text = item.format.format_note
|
||||
|
||||
if (item.format.container != "") container!!.text = item.format.container.uppercase()
|
||||
else container!!.visibility = View.GONE
|
||||
|
||||
val codecText =
|
||||
if (item.format.encoding != "") {
|
||||
item.format.encoding.uppercase()
|
||||
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
|
||||
item.format.vcodec.uppercase()
|
||||
} else {
|
||||
item.format.acodec.uppercase()
|
||||
}
|
||||
if (codecText == "" || codecText == "none"){
|
||||
codec!!.visibility = View.GONE
|
||||
}else{
|
||||
codec!!.visibility = View.VISIBLE
|
||||
codec.text = codecText
|
||||
}
|
||||
|
||||
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
|
||||
else fileSize!!.text = fileSizeReadable
|
||||
|
||||
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
val url = item.url
|
||||
link!!.text = url
|
||||
link.tag = itemID
|
||||
link.setOnClickListener{
|
||||
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
|
||||
}
|
||||
link.setOnLongClickListener{
|
||||
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
|
||||
true
|
||||
}
|
||||
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
|
||||
remove!!.tag = itemID
|
||||
remove.setOnClickListener{
|
||||
removeItem(item, bottomSheet)
|
||||
}
|
||||
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
|
||||
openFile!!.visibility = View.GONE
|
||||
|
||||
val download = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
|
||||
download!!.text = getString(R.string.download)
|
||||
download.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
|
||||
download.tag = itemID
|
||||
download.setOnClickListener{
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
bottomSheet.cancel()
|
||||
}
|
||||
|
||||
download.setOnLongClickListener {
|
||||
bottomSheet.cancel()
|
||||
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(item), item.type, item, false)
|
||||
sheet.show(parentFragmentManager, "downloadSingleSheet")
|
||||
true
|
||||
}
|
||||
|
||||
openFile.visibility = View.GONE
|
||||
|
||||
bottomSheet.show()
|
||||
val displayMetrics = DisplayMetrics()
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
bottomSheet.window!!.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
|
||||
val item = items.find { it.id == itemID }
|
||||
if (isChecked) {
|
||||
selectedObjects!!.add(item!!)
|
||||
if (actionMode == null){
|
||||
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
|
||||
|
||||
}else{
|
||||
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
selectedObjects!!.remove(item)
|
||||
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
if (selectedObjects!!.isEmpty()){
|
||||
actionMode?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun removeItem(item: DownloadItem, bottomSheet: BottomSheetDialog?){
|
||||
bottomSheet?.hide()
|
||||
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 ->
|
||||
downloadViewModel.deleteDownload(item)
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
|
||||
private val contextualActionBar = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
mode!!.menuInflater.inflate(R.menu.saved_downloads_menu_context, menu)
|
||||
mode.title = "${selectedObjects!!.size} ${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.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 ->
|
||||
for (obj in selectedObjects!!){
|
||||
downloadViewModel.deleteDownload(obj)
|
||||
}
|
||||
clearCheckedItems()
|
||||
actionMode?.finish()
|
||||
}
|
||||
deleteDialog.show()
|
||||
true
|
||||
}
|
||||
R.id.download -> {
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList())
|
||||
actionMode?.finish()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.select_all -> {
|
||||
savedDownloads.checkAll(items)
|
||||
selectedObjects?.clear()
|
||||
items.forEach { selectedObjects?.add(it) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
savedDownloads.invertSelected(items)
|
||||
val invertedList = arrayListOf<DownloadItem>()
|
||||
items.forEach {
|
||||
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
|
||||
}
|
||||
selectedObjects?.clear()
|
||||
selectedObjects?.addAll(invertedList)
|
||||
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
if (invertedList.isEmpty()) actionMode?.finish()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
actionMode = null
|
||||
clearCheckedItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCheckedItems(){
|
||||
savedDownloads.clearCheckeditems()
|
||||
selectedObjects?.clear()
|
||||
}
|
||||
|
||||
private var simpleCallback: ItemTouchHelper.SimpleCallback =
|
||||
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
||||
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
when (direction) {
|
||||
ItemTouchHelper.LEFT -> {
|
||||
val deletedItem = items[position]
|
||||
downloadViewModel.deleteDownload(deletedItem)
|
||||
Snackbar.make(savedRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
downloadViewModel.insert(deletedItem)
|
||||
}.show()
|
||||
}
|
||||
ItemTouchHelper.RIGHT -> {
|
||||
onActionButtonClick(items[position].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
|
||||
)
|
||||
)
|
||||
.addSwipeRightActionIcon(R.drawable.ic_downloads)
|
||||
.create()
|
||||
.decorate()
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder!!,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.deniscerri.ytdlnis.ui.more.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package com.deniscerri.ytdlnis.ui.more.settings
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Bundle
|
||||
|
|
@ -14,9 +18,10 @@ import androidx.work.WorkInfo
|
|||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdlnis.MainActivity
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.ui.more.TerminalActivity
|
||||
import com.deniscerri.ytdlnis.util.ThemeUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.google.android.gms.common.wrappers.Wrappers.packageManager
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
|
|
@ -28,6 +33,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
|||
private var accent: ListPreference? = null
|
||||
private var highContrast: SwitchPreferenceCompat? = null
|
||||
private var locale: ListPreference? = null
|
||||
private var hideTerminal: SwitchPreferenceCompat? = null
|
||||
|
||||
private var updateUtil: UpdateUtil? = null
|
||||
private var activeDownloadCount = 0
|
||||
|
|
@ -48,6 +54,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
|||
accent = findPreference("theme_accent")
|
||||
highContrast = findPreference("high_contrast")
|
||||
locale = findPreference("locale")
|
||||
hideTerminal = findPreference("hide_terminal")
|
||||
|
||||
if(VERSION.SDK_INT < VERSION_CODES.TIRAMISU){
|
||||
val values = resources.getStringArray(R.array.language_values)
|
||||
|
|
@ -108,5 +115,21 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
|||
true
|
||||
}
|
||||
|
||||
hideTerminal!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { pref: Preference?, _: Any ->
|
||||
val packageManager = requireContext().packageManager
|
||||
val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdlnis.terminalShareAlias")
|
||||
|
||||
if ((pref as SwitchPreferenceCompat).isChecked){
|
||||
packageManager.setComponentEnabledSetting(aliasComponentName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
}else{
|
||||
packageManager.setComponentEnabledSetting(aliasComponentName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +64,6 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
|||
private lateinit var cookieViewModel: CookieViewModel
|
||||
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
|
||||
|
||||
private var arch: Preference? = null
|
||||
private var version: Preference? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
|
@ -201,9 +200,8 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
version = findPreference("version")
|
||||
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
|
||||
val installedABI = Build.SUPPORTED_ABIS.first { nativeLibraryDir!!.contains(it) }
|
||||
|
||||
version!!.summary = "${BuildConfig.VERSION_NAME} [${installedABI}] ${BuildConfig.BUILD_TYPE}"
|
||||
version!!.summary = "${BuildConfig.VERSION_NAME} [${nativeLibraryDir?.split("/lib/")?.get(1)}] ${BuildConfig.BUILD_TYPE}"
|
||||
|
||||
version!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
|
|
|
|||
|
|
@ -34,12 +34,10 @@ import java.util.regex.Pattern
|
|||
class InfoUtil(private val context: Context) {
|
||||
private var items: ArrayList<ResultItem?>
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private var key: String? = null
|
||||
|
||||
init {
|
||||
try {
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
key = sharedPreferences.getString("api_key", "")
|
||||
countryCODE = sharedPreferences.getString("locale", "")!!
|
||||
if (countryCODE.isEmpty()) countryCODE = "US"
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -51,70 +49,44 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
fun search(query: String): ArrayList<ResultItem?> {
|
||||
items = ArrayList()
|
||||
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
|
||||
return if (searchEngine == "ytsearch"){
|
||||
if (key!!.isNotEmpty()) searchFromKey(query) else {
|
||||
try{
|
||||
searchFromPiped(query)
|
||||
}catch (e: Exception){
|
||||
getFromYTDL(query)
|
||||
}
|
||||
return when(sharedPreferences.getString("search_engine", "ytsearch")){
|
||||
"ytsearch" -> try{
|
||||
searchFromPiped(query)
|
||||
}catch (e: Exception){
|
||||
getFromYTDL(query)
|
||||
}
|
||||
}else getFromYTDL(query)
|
||||
|
||||
"ytsearchmusic" -> try{
|
||||
searchFromPipedMusic(query)
|
||||
}catch (e: Exception){
|
||||
getFromYTDL(query)
|
||||
}
|
||||
|
||||
else -> getFromYTDL(query)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun searchFromKey(query: String): ArrayList<ResultItem?> {
|
||||
//short data
|
||||
val res =
|
||||
genericRequest("https://youtube.googleapis.com/youtube/v3/search?part=snippet&q=$query&maxResults=25®ionCode=$countryCODE&key=$key")
|
||||
if (!res.has("items")) return getFromYTDL(query)
|
||||
val dataArray = res.getJSONArray("items")
|
||||
|
||||
//extra data
|
||||
var url2 = "https://www.googleapis.com/youtube/v3/videos?id="
|
||||
//getting all ids, for the extra data request
|
||||
fun searchFromPiped(query: String): ArrayList<ResultItem?> {
|
||||
val data = genericRequest("$pipedURL/search?q=$query&filter=videos")
|
||||
val dataArray = data.getJSONArray("items")
|
||||
if (dataArray.length() == 0) return getFromYTDL(query)
|
||||
for (i in 0 until dataArray.length()) {
|
||||
val element = dataArray.getJSONObject(i)
|
||||
val snippet = element.getJSONObject("snippet")
|
||||
if (element.getJSONObject("id").getString("kind") == "youtube#video") {
|
||||
val videoID = element.getJSONObject("id").getString("videoId")
|
||||
url2 = "$url2$videoID,"
|
||||
snippet.put("videoID", videoID)
|
||||
}
|
||||
}
|
||||
url2 = url2.substring(
|
||||
0,
|
||||
url2.length - 1
|
||||
) + "&part=contentDetails®ionCode=" + countryCODE + "&key=" + key
|
||||
val extra = genericRequest(url2)
|
||||
var j = 0
|
||||
for (i in 0 until dataArray.length()) {
|
||||
val element = dataArray.getJSONObject(i)
|
||||
val snippet = element.getJSONObject("snippet")
|
||||
if (element.getJSONObject("id").getString("kind") == "youtube#video") {
|
||||
var duration =
|
||||
extra.getJSONArray("items").getJSONObject(j++).getJSONObject("contentDetails")
|
||||
.getString("duration")
|
||||
duration = formatDuration(duration)
|
||||
if (duration == "0:00") {
|
||||
continue
|
||||
}
|
||||
snippet.put("duration", duration)
|
||||
fixThumbnail(snippet)
|
||||
val v = createVideofromJSON(snippet)
|
||||
if (v == null || v.thumb.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
items.add(createVideofromJSON(snippet))
|
||||
if (element.getInt("duration") == -1) continue
|
||||
element.put("uploader", element.getString("uploaderName"))
|
||||
val v = createVideoFromPipedJSON(element, element.getString("url").removePrefix("/watch?v="))
|
||||
if (v == null || v.thumb.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
items.add(v)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun searchFromPiped(query: String): ArrayList<ResultItem?> {
|
||||
val data = genericRequest("$pipedURL/search?q=$query?filter=videos")
|
||||
fun searchFromPipedMusic(query: String): ArrayList<ResultItem?> {
|
||||
val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs")
|
||||
val dataArray = data.getJSONArray("items")
|
||||
if (dataArray.length() == 0) return getFromYTDL(query)
|
||||
for (i in 0 until dataArray.length()) {
|
||||
|
|
@ -134,77 +106,24 @@ class InfoUtil(private val context: Context) {
|
|||
fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple {
|
||||
try{
|
||||
items = ArrayList()
|
||||
if (key!!.isEmpty()) {
|
||||
// -------------- PIPED API FUNCTION -------------------
|
||||
var url = ""
|
||||
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
|
||||
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""
|
||||
// -------------- PIPED API FUNCTION -------------------
|
||||
var url = ""
|
||||
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
|
||||
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""
|
||||
|
||||
val res = genericRequest(url)
|
||||
if (!res.has("relatedStreams")) throw Exception()
|
||||
|
||||
val dataArray = res.getJSONArray("relatedStreams")
|
||||
var nextpage = res.getString("nextpage")
|
||||
for (i in 0 until dataArray.length()){
|
||||
val obj = dataArray.getJSONObject(i)
|
||||
val itm = createVideoFromPipedJSON(obj, obj.getString("url").removePrefix("/watch?v="))
|
||||
itm?.playlistTitle = "YTDLnis"
|
||||
items.add(itm)
|
||||
}
|
||||
if (nextpage == "null") nextpage = ""
|
||||
return PlaylistTuple(nextpage, items)
|
||||
}
|
||||
|
||||
//---------- YOUTUBE API FUNCTION --------------------------------
|
||||
val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50®ionCode=$countryCODE&playlistId=$id&key=$key"
|
||||
//short data
|
||||
val res = genericRequest(url)
|
||||
if (!res.has("items")) return PlaylistTuple(
|
||||
"",
|
||||
getFromYTDL("https://www.youtube.com/playlist?list=$id")
|
||||
)
|
||||
val dataArray = res.getJSONArray("items")
|
||||
if (!res.has("relatedStreams")) throw Exception()
|
||||
|
||||
//extra data
|
||||
var url2 = "https://www.googleapis.com/youtube/v3/videos?id="
|
||||
//getting all ids, for the extra data request
|
||||
for (i in 0 until dataArray.length()) {
|
||||
val element = dataArray.getJSONObject(i)
|
||||
val snippet = element.getJSONObject("snippet")
|
||||
val videoID = snippet.getJSONObject("resourceId").getString("videoId")
|
||||
url2 = "$url2$videoID,"
|
||||
snippet.put("videoID", videoID)
|
||||
val dataArray = res.getJSONArray("relatedStreams")
|
||||
var nextpage = res.getString("nextpage")
|
||||
for (i in 0 until dataArray.length()){
|
||||
val obj = dataArray.getJSONObject(i)
|
||||
val itm = createVideoFromPipedJSON(obj, obj.getString("url").removePrefix("/watch?v="))
|
||||
itm?.playlistTitle = "YTDLnis"
|
||||
items.add(itm)
|
||||
}
|
||||
url2 = url2.substring(
|
||||
0,
|
||||
url2.length - 1
|
||||
) + "&part=contentDetails®ionCode=" + countryCODE + "&key=" + key
|
||||
val extra = genericRequest(url2)
|
||||
val extraArray = extra.getJSONArray("items")
|
||||
var j = 0
|
||||
var i = 0
|
||||
while (i < extraArray.length()) {
|
||||
val element = dataArray.getJSONObject(i)
|
||||
val snippet = element.getJSONObject("snippet")
|
||||
var duration =
|
||||
extra.getJSONArray("items").getJSONObject(j).getJSONObject("contentDetails")
|
||||
.getString("duration")
|
||||
duration = formatDuration(duration)
|
||||
snippet.put("duration", duration)
|
||||
fixThumbnail(snippet)
|
||||
val v = createVideofromJSON(snippet)
|
||||
if (v == null || v.thumb.isEmpty()) {
|
||||
i++
|
||||
continue
|
||||
} else {
|
||||
j++
|
||||
}
|
||||
v.playlistTitle = "YTDLnis"
|
||||
items.add(v)
|
||||
i++
|
||||
}
|
||||
val next = res.optString("nextPageToken")
|
||||
return PlaylistTuple(next, items)
|
||||
if (nextpage == "null") nextpage = ""
|
||||
return PlaylistTuple(nextpage, items)
|
||||
}catch (e: Exception){
|
||||
return PlaylistTuple(
|
||||
"",
|
||||
|
|
@ -215,29 +134,14 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
@Throws(JSONException::class)
|
||||
fun getVideo(url: String): List<ResultItem?> {
|
||||
try {
|
||||
return try {
|
||||
val id = getIDFromYoutubeURL(url)
|
||||
if (key!!.isEmpty()) {
|
||||
val res = genericRequest("$pipedURL/streams/$id")
|
||||
return if (res.length() == 0) getFromYTDL(url) else listOf(createVideoFromPipedJSON(
|
||||
res, id
|
||||
))
|
||||
}
|
||||
|
||||
//short data
|
||||
var res =
|
||||
genericRequest("https://youtube.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=$id&key=$key")
|
||||
if (!res.has("items")) return getFromYTDL(url)
|
||||
var duration = res.getJSONArray("items").getJSONObject(0).getJSONObject("contentDetails")
|
||||
.getString("duration")
|
||||
duration = formatDuration(duration)
|
||||
res = res.getJSONArray("items").getJSONObject(0).getJSONObject("snippet")
|
||||
res.put("videoID", id)
|
||||
res.put("duration", duration)
|
||||
fixThumbnail(res)
|
||||
return listOf(createVideofromJSON(res))
|
||||
val res = genericRequest("$pipedURL/streams/$id")
|
||||
if (res.length() == 0) getFromYTDL(url) else listOf(createVideoFromPipedJSON(
|
||||
res, id
|
||||
))
|
||||
}catch (e: Exception){
|
||||
return getFromYTDL(url)
|
||||
getFromYTDL(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,6 +207,7 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
formats.add(formatObj)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (obj.has("videoStreams")){
|
||||
|
|
@ -318,6 +223,7 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
formats.add(formatObj)
|
||||
}
|
||||
|
||||
}
|
||||
formats.sortBy { it.filesize }
|
||||
formats.groupBy { it.format_id }.forEach {
|
||||
|
|
@ -442,42 +348,9 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun getTrending(context: Context): ArrayList<ResultItem?> {
|
||||
|
||||
fun getTrending(): ArrayList<ResultItem?> {
|
||||
items = ArrayList()
|
||||
return if (key!!.isEmpty()) {
|
||||
getTrendingFromPiped()
|
||||
} else getTrendingFromKey(context)
|
||||
}
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun getTrendingFromKey(context: Context): ArrayList<ResultItem?> {
|
||||
val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10®ionCode=$countryCODE&maxResults=25&key=$key"
|
||||
//short data
|
||||
val res = genericRequest(url)
|
||||
//extra data from the same videos
|
||||
val contentDetails =
|
||||
genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10®ionCode=$countryCODE&maxResults=25&key=$key")
|
||||
if (!contentDetails.has("items")) return ArrayList()
|
||||
val dataArray = res.getJSONArray("items")
|
||||
val extraDataArray = contentDetails.getJSONArray("items")
|
||||
for (i in 0 until dataArray.length()) {
|
||||
val element = dataArray.getJSONObject(i)
|
||||
val snippet = element.getJSONObject("snippet")
|
||||
var duration = extraDataArray.getJSONObject(i).getJSONObject("contentDetails")
|
||||
.getString("duration")
|
||||
duration = formatDuration(duration)
|
||||
snippet.put("videoID", element.getString("id"))
|
||||
snippet.put("duration", duration)
|
||||
fixThumbnail(snippet)
|
||||
val v = createVideofromJSON(snippet)
|
||||
if (v == null || v.thumb.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
v.playlistTitle = context.getString(R.string.trendingPlaylist)
|
||||
items.add(v)
|
||||
}
|
||||
return items
|
||||
return getTrendingFromPiped()
|
||||
}
|
||||
|
||||
private fun getTrendingFromPiped(): ArrayList<ResultItem?> {
|
||||
|
|
@ -521,13 +394,17 @@ class InfoUtil(private val context: Context) {
|
|||
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
|
||||
val m = p.matcher(url)
|
||||
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
|
||||
return if(m.find() && formatSource == "piped"){
|
||||
val id = getIDFromYoutubeURL(url)
|
||||
val res = genericRequest(pipedURL + "/streams/" + id)
|
||||
if (res.length() == 0) getFromYTDL(url)[0]!!
|
||||
val item = createVideoFromPipedJSON(res, id)
|
||||
item!!.formats
|
||||
}else{
|
||||
return try {
|
||||
if(m.find() && formatSource == "piped"){
|
||||
val id = getIDFromYoutubeURL(url)
|
||||
val res = genericRequest("$pipedURL/streams/$id")
|
||||
if (res.length() == 0) getFromYTDL(url)[0]!!
|
||||
val item = createVideoFromPipedJSON(res, id)
|
||||
item!!.formats
|
||||
}else{
|
||||
getFormatsFromYTDL(url)
|
||||
}
|
||||
}catch (e: Exception){
|
||||
getFormatsFromYTDL(url)
|
||||
}
|
||||
}
|
||||
|
|
@ -683,13 +560,24 @@ class InfoUtil(private val context: Context) {
|
|||
items = ArrayList()
|
||||
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
|
||||
try {
|
||||
val request = YoutubeDLRequest(query)
|
||||
val request : YoutubeDLRequest
|
||||
if (query.contains("http")){
|
||||
request = YoutubeDLRequest(query)
|
||||
}else{
|
||||
request = YoutubeDLRequest(emptyList())
|
||||
if (searchEngine == "ytsearchmusic"){
|
||||
request.addOption("--default-search", "https://music.youtube.com/search?q=")
|
||||
request.addOption("ytsearch25:\"${query}\"")
|
||||
}else{
|
||||
request.addOption("${searchEngine}25:\"${query}\"")
|
||||
}
|
||||
}
|
||||
|
||||
request.addOption("--flat-playlist")
|
||||
request.addOption("-j")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
if (!query.contains("http")) request.addOption("--default-search", "${searchEngine}25")
|
||||
|
||||
val cookiesFile = File(context.cacheDir, "cookies.txt")
|
||||
if (cookiesFile.exists()){
|
||||
|
|
@ -1047,7 +935,8 @@ class InfoUtil(private val context: Context) {
|
|||
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
|
||||
}
|
||||
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
|
||||
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader)s - %(title)s"
|
||||
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader|${downloadItem.author})s - %(title)s"
|
||||
downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader|${downloadItem.author})s")
|
||||
|
||||
if (downloadItem.downloadSections.isNotBlank()){
|
||||
downloadItem.downloadSections.split(";").forEach {
|
||||
|
|
@ -1142,11 +1031,12 @@ class InfoUtil(private val context: Context) {
|
|||
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
|
||||
}
|
||||
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
|
||||
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
}else{
|
||||
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
request.addOption("-o", "${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1194,9 +1084,11 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
|
||||
if (downloadItem.videoPreferences.writeSubs){
|
||||
val subFormat = sharedPreferences.getString("sub_format", "srt")
|
||||
|
||||
request.addOption("--write-subs")
|
||||
request.addOption("--write-auto-subs")
|
||||
request.addOption("--sub-format", "str/ass/best")
|
||||
request.addOption("--sub-format", "${subFormat}/best")
|
||||
request.addOption("--convert-subtitles", "srt")
|
||||
if (!downloadItem.videoPreferences.embedSubs) {
|
||||
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
|
||||
|
|
@ -1207,11 +1099,12 @@ class InfoUtil(private val context: Context) {
|
|||
request.addOption("--ppa", "ffmpeg:-an")
|
||||
}
|
||||
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
|
||||
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
}else{
|
||||
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
request.addOption("-o", "${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,39 @@
|
|||
package com.deniscerri.ytdlnis.util
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.text.Spanned
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.google.android.material.color.DynamicColors
|
||||
|
||||
|
||||
object ThemeUtil {
|
||||
|
||||
sealed class AppIcon(
|
||||
@DrawableRes val iconResource: Int,
|
||||
val activityAlias: String
|
||||
) {
|
||||
object Default : AppIcon(R.mipmap.ic_launcher, "Default")
|
||||
object Light : AppIcon(R.mipmap.ic_launcher_light, "LightIcon")
|
||||
object Dark : AppIcon(R.mipmap.ic_launcher_dark, "DarkIcon")
|
||||
}
|
||||
|
||||
private val availableIcons = listOf(
|
||||
AppIcon.Default,
|
||||
AppIcon.Light,
|
||||
AppIcon.Dark
|
||||
)
|
||||
|
||||
fun updateTheme(activity: AppCompatActivity) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
|
||||
|
|
@ -31,12 +56,79 @@ object ThemeUtil {
|
|||
activity.theme.applyStyle(R.style.Pure, true)
|
||||
}
|
||||
|
||||
//disable old icons
|
||||
for (appIcon in availableIcons) {
|
||||
val activityClass = "com.deniscerri.ytdlnis." + appIcon.activityAlias
|
||||
|
||||
// remove old icons
|
||||
activity.packageManager.setComponentEnabledSetting(
|
||||
ComponentName(activity.packageName, activityClass),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
|
||||
when (sharedPreferences.getString("ytdlnis_theme", "System")!!) {
|
||||
"System" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
"Light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
"Dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
"System" -> {
|
||||
//set dynamic icon
|
||||
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.Default")
|
||||
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
"Light" -> {
|
||||
//set light icon
|
||||
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.LightIcon")
|
||||
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
"Dark" -> {
|
||||
//set dark icon
|
||||
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.DarkIcon")
|
||||
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
else -> {
|
||||
//set dynamic icon
|
||||
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.Default")
|
||||
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getThemeColor(context: Context, colorCode: Int): Int {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val accent = sharedPreferences.getString("theme_accent", "blue")
|
||||
return if (accent == "Default" || accent == "blue"){
|
||||
"c22727".toInt(16)
|
||||
}else{
|
||||
val value = TypedValue()
|
||||
context.theme.resolveAttribute(colorCode, value, true)
|
||||
value.data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the styled app name
|
||||
*/
|
||||
fun getStyledAppName(context: Context): Spanned {
|
||||
val colorPrimary = getThemeColor(context, androidx.appcompat.R.attr.colorPrimaryDark)
|
||||
val hexColor = "#%06X".format(0xFFFFFF and colorPrimary)
|
||||
return "<span style='color:$hexColor';>YTDL</span>nis"
|
||||
.parseAsHtml(HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
}
|
||||
|
|
@ -54,12 +54,20 @@ import java.util.Calendar
|
|||
|
||||
object UiUtil {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun populateFormatCard(formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>?){
|
||||
formatCard.findViewById<TextView>(R.id.container).text = chosenFormat.container.uppercase()
|
||||
fun populateFormatCard(context: Context, formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>?){
|
||||
var formatNote = chosenFormat.format_note
|
||||
if (formatNote.isEmpty()) formatNote = context.getString(R.string.defaultValue)
|
||||
else if (formatNote == "best") formatNote = context.getString(R.string.best_quality)
|
||||
else if (formatNote == "worst") formatNote = context.getString(R.string.worst_quality)
|
||||
|
||||
var container = chosenFormat.container
|
||||
if (container == "Default") container = context.getString(R.string.defaultValue)
|
||||
|
||||
formatCard.findViewById<TextView>(R.id.container).text = container.uppercase()
|
||||
if (audioFormats.isNullOrEmpty()){
|
||||
formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase()
|
||||
formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase()
|
||||
}else{
|
||||
val title = "${chosenFormat.format_note.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]"
|
||||
val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]"
|
||||
formatCard.findViewById<TextView>(R.id.format_note).text = title
|
||||
}
|
||||
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.deniscerri.ytdlnis.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.cronet.CronetDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.deniscerri.ytdlnis.App
|
||||
import org.chromium.net.CronetEngine
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object VideoPlayerUtil {
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
fun buildPlayer(context: Context) : ExoPlayer {
|
||||
val player: ExoPlayer
|
||||
|
||||
val cronetEngine: CronetEngine = CronetEngine.Builder(App.instance)
|
||||
.enableHttp2(true)
|
||||
.enableQuic(true)
|
||||
.enableBrotli(true)
|
||||
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 1024L * 1024L) // 1MiB
|
||||
.build()
|
||||
|
||||
val trackSelector = DefaultTrackSelector(context)
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
// cache the last three minutes
|
||||
.setBackBuffer(1000 * 60 * 3, true)
|
||||
.setBufferDurationsMs(
|
||||
1000 * 10, // exo default is 50s
|
||||
50000,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
|
||||
val cronetDataSourceFactory = CronetDataSource.Factory(
|
||||
cronetEngine,
|
||||
Executors.newCachedThreadPool()
|
||||
)
|
||||
val dataSourceFactory = DefaultDataSource.Factory(context, cronetDataSourceFactory)
|
||||
|
||||
player = ExoPlayer.Builder(context)
|
||||
.setUsePlatformDiagnostics(false)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
,
|
||||
true)
|
||||
.build()
|
||||
|
||||
return player
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.deniscerri.ytdlnis.work
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.Converters
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.DownloadDao
|
||||
import com.deniscerri.ytdlnis.database.dao.ResultDao
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.LogItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.LogRepository
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
|
||||
class UpdatePlaylistFormatsWorker(
|
||||
private val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
val dbManager = DBManager.getInstance(context)
|
||||
val resultDao = dbManager.resultDao
|
||||
val infoUtil = InfoUtil(context)
|
||||
val converters = Converters()
|
||||
val urls = inputData.getStringArray("urls")!!.toMutableList()
|
||||
|
||||
infoUtil.getFormatsMultiple(urls){
|
||||
val formatStrings = mutableListOf<String>()
|
||||
it.forEach { i -> formatStrings.add(converters.formatToString(i)) }
|
||||
setProgressAsync(workDataOf("formats" to formatStrings.toTypedArray()))
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val result = resultDao.getResultByURL(urls.removeFirst())
|
||||
val formats = infoUtil.getFormats(result.url)
|
||||
result.formats = ArrayList(formats)
|
||||
resultDao.update(result)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.deniscerri.ytdlnis.work
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.DownloadDao
|
||||
import com.deniscerri.ytdlnis.database.dao.ResultDao
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.LogItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.LogRepository
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
|
||||
class UpdateYTDLWorker(
|
||||
private val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
UpdateUtil(context).updateYoutubeDL()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
}
|
||||
14
app/src/main/res/anim/fade_in.xml
Normal file
14
app/src/main/res/anim/fade_in.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fillAfter="true" >
|
||||
|
||||
<scale
|
||||
android:duration="200"
|
||||
android:fromXScale="0.8"
|
||||
android:fromYScale="0.8"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="1"
|
||||
android:toYScale="1" >
|
||||
</scale>
|
||||
|
||||
</set>
|
||||
5
app/src/main/res/drawable/baseline_format_quote_24.xml
Normal file
5
app/src/main/res/drawable/baseline_format_quote_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z"/>
|
||||
</vector>
|
||||
6
app/src/main/res/drawable/baseline_hide_image_24.xml
Normal file
6
app/src/main/res/drawable/baseline_hide_image_24.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M21,5c0,-1.1 -0.9,-2 -2,-2H5.83L21,18.17V5z"/>
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M2.81,2.81L1.39,4.22L3,5.83V19c0,1.1 0.9,2 2,2h13.17l1.61,1.61l1.41,-1.41L2.81,2.81zM6,17l3,-4l2.25,3l0.82,-1.1l2.1,2.1H6z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_launcher_background_dark.xml
Normal file
10
app/src/main/res/drawable/ic_launcher_background_dark.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M 0 0 H 192 V 192 H 0 V 0 Z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_launcher_background_light.xml
Normal file
10
app/src/main/res/drawable/ic_launcher_background_light.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M 0 0 H 192 V 192 H 0 V 0 Z" />
|
||||
</vector>
|
||||
167
app/src/main/res/layout/active_download_card_minified.xml
Normal file
167
app/src/main/res/layout/active_download_card_minified.xml
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/active_download_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checkable="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:checkedIcon="@null"
|
||||
app:strokeColor="?attr/colorPrimary"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardElevation="10dp"
|
||||
app:cardMaxElevation="12dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:alpha="0.3"
|
||||
android:scaleY="200"
|
||||
app:trackColor="#000" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="?attr/colorSurfaceVariant"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
|
||||
app:layout_constraintHorizontal_weight="0.3"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:background="#80000000"
|
||||
android:clickable="false"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLength="17"
|
||||
android:maxLines="1"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/image_view"
|
||||
app:layout_constraintEnd_toEndOf="@+id/image_view" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_item_data"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/active_download_cancel"
|
||||
app:layout_constraintHorizontal_weight="0.7"
|
||||
app:layout_constraintStart_toEndOf="@+id/image_view"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:scrollbars="none"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/format_note"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:backgroundTint="?attr/colorSecondary"
|
||||
android:clickable="false"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLength="17"
|
||||
android:maxLines="1"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/active_download_cancel"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:backgroundTint="?attr/colorSurface"
|
||||
app:cornerRadius="15dp"
|
||||
app:icon="@drawable/ic_cancel"
|
||||
app:iconSize="30dp"
|
||||
app:iconTint="?android:textColorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="download_item_data,active_download_cancel" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="200dp"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
android:scrollbars="none"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/action_button"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_type"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="title,action_button" />
|
||||
app:constraint_referenced_ids="title,download_type" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
225
app/src/main/res/layout/result_card_details.xml
Normal file
225
app/src/main/res/layout/result_card_details.xml
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:paddingBottom="20dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/frame_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:resize_mode="fixed_height"
|
||||
app:useDefaultControls="true"
|
||||
app:use_controller="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_thumb"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_image"
|
||||
app:iconSize="30dp"
|
||||
app:iconTint="?android:colorAccent"
|
||||
app:layout_constraintTop_toTopOf="@+id/frame_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pause"
|
||||
android:visibility="gone"
|
||||
android:clickable="false"
|
||||
app:iconSize="50dp"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
app:iconTint="?android:colorAccent"
|
||||
app:icon="@drawable/exomedia_ic_play_arrow_white"
|
||||
android:layout_width="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:translationZ="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/frame_layout"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/list_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginVertical="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:singleLine="false"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_button_layout"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="false"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_button_layout"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/download_button_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_music"
|
||||
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="55dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
app:icon="@drawable/ic_music" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_video"
|
||||
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="55dp"
|
||||
android:layout_margin="5dp"
|
||||
app:icon="@drawable/ic_video" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/title_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="bottom_info,download_button_layout" />
|
||||
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_barrier">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/running"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:text="@string/running"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/running_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/running" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/queued"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:text="@string/download_queue"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/running_recycler" />
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/queued_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/queued" />
|
||||
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/bottom_sheet_link"
|
||||
style="@style/Widget.Material3.Button.TextButton.Icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/app_name"
|
||||
android:textAlignment="textStart"
|
||||
android:textSize="15sp"
|
||||
app:icon="@drawable/ic_link"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/queued_recycler" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -21,4 +21,10 @@
|
|||
android:icon="@drawable/ic_delete_all"
|
||||
android:title="@string/clear_errored"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/clear_saved"
|
||||
android:icon="@drawable/ic_delete_all"
|
||||
android:title="@string/clear_saved"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
29
app/src/main/res/menu/saved_downloads_menu_context.xml
Normal file
29
app/src/main/res/menu/saved_downloads_menu_context.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<menu 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"
|
||||
tools:context=".MainActivity" >
|
||||
|
||||
<item
|
||||
android:id="@+id/delete_results"
|
||||
android:title="@string/remove_results"
|
||||
android:icon="@drawable/baseline_delete_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/download"
|
||||
android:title="@string/download"
|
||||
android:icon="@drawable/baseline_download_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/select_all"
|
||||
android:title="@string/select_all"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/invert_selected"
|
||||
android:title="@string/invert_selected"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_dark.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_dark.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_dark"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_light.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_light.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_light"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_dark"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_light"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -28,18 +28,23 @@
|
|||
android:label="MainSettingsFragment" >
|
||||
<action
|
||||
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
|
||||
app:destination="@id/appearanceSettingsFragment" />
|
||||
app:destination="@id/appearanceSettingsFragment"
|
||||
app:enterAnim="@anim/fade_in" />
|
||||
<action
|
||||
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
|
||||
app:destination="@id/folderSettingsFragment" />
|
||||
app:destination="@id/folderSettingsFragment"
|
||||
app:enterAnim="@anim/fade_in" />
|
||||
<action
|
||||
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
|
||||
app:destination="@id/downloadSettingsFragment" />
|
||||
app:destination="@id/downloadSettingsFragment"
|
||||
app:enterAnim="@anim/fade_in" />
|
||||
<action
|
||||
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
|
||||
app:destination="@id/processingSettingsFragment" />
|
||||
app:destination="@id/processingSettingsFragment"
|
||||
app:enterAnim="@anim/fade_in" />
|
||||
<action
|
||||
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
|
||||
app:destination="@id/updateSettingsFragment" />
|
||||
app:destination="@id/updateSettingsFragment"
|
||||
app:enterAnim="@anim/fade_in" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
|
|
@ -271,7 +271,7 @@
|
|||
<string name="move_temporary_files_summary">Transfero skedarët e përkohshëm në dosjen e shkarkimeve</string>
|
||||
<string name="format_filtering_hint_2">Të gjithë artikujt duhet të jenë audio ose video për të përdorur këtë opsion</string>
|
||||
<string name="custom_audio_quality">Përdor cilësinë e personalizuar të audios</string>
|
||||
<string name="piped_instance">Shembull me tubacione</string>
|
||||
<string name="piped_instance">Instanca e PIPED</string>
|
||||
<string name="piped_instance_summary">Shkruani një Server API PIPED, aplikacioni mund të përdorë për kërkimet dhe formatet në youtube</string>
|
||||
<string name="retries">Riprovimet</string>
|
||||
<string name="fragment_retries">Riprovimet e fragmentit</string>
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@
|
|||
|
||||
<array name="search_engines">
|
||||
<item>YouTube</item>
|
||||
<item>YouTube Music</item>
|
||||
<item>Nico video</item>
|
||||
<item>PRX Series</item>
|
||||
<item>PRX Stories</item>
|
||||
|
|
@ -170,10 +171,14 @@
|
|||
<item>Google Video</item>
|
||||
<item>Bilibili</item>
|
||||
<item>Netverse</item>
|
||||
<item>All Clips of a trovo.live channel</item>
|
||||
<item>All VODs of a trovo.live channel</item>
|
||||
<item>web.archive.org saved youtube videos</item>
|
||||
</array>
|
||||
|
||||
<array name="search_engines_values">
|
||||
<item>ytsearch</item>
|
||||
<item>ytsearchmusic</item>
|
||||
<item>nicosearch</item>
|
||||
<item>PRX Series</item>
|
||||
<item>PRX Stories</item>
|
||||
|
|
@ -183,6 +188,9 @@
|
|||
<item>gvsearch</item>
|
||||
<item>bilisearch</item>
|
||||
<item>netsearch</item>
|
||||
<item>trovoclip</item>
|
||||
<item>trovovod</item>
|
||||
<item>ytarchive</item>
|
||||
</array>
|
||||
|
||||
<array name="start_destination">
|
||||
|
|
@ -740,4 +748,23 @@
|
|||
<item>searchHistory</item>
|
||||
</array>
|
||||
|
||||
<string-array name="sub_formats">
|
||||
<item>srt</item>
|
||||
<item>ass</item>
|
||||
<item>lrc</item>
|
||||
<item>vtt</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="hide_thumbnail">
|
||||
<item>@string/home</item>
|
||||
<item>@string/downloads</item>
|
||||
<item>@string/download_queue</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="hide_thumbnail_values">
|
||||
<item>home</item>
|
||||
<item>downloads</item>
|
||||
<item>queue</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
@ -208,7 +208,7 @@
|
|||
<string name="subtitle_languages">Subtitle languages</string>
|
||||
<string name="format_source">Formats Source</string>
|
||||
<string name="video_recommendations">Video Recommendations</string>
|
||||
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen from Invidious</string>
|
||||
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen from PIPED API</string>
|
||||
<string name="preferred_search_engine">Preferred Search Engine</string>
|
||||
<string name="preferred_search_engine_summary">The search engine to use for in-app searches</string>
|
||||
<string name="format_filtering_hint">All items must be of the same type to use this option</string>
|
||||
|
|
@ -283,8 +283,14 @@
|
|||
<string name="fragment_retries">Fragment Retries</string>
|
||||
<string name="format_order">Format Order</string>
|
||||
<string name="force_keyframes">Force Keyframes At Cuts</string>
|
||||
<string name="force_keyframes_summary">Accurate Video Cuts. Slower</string>
|
||||
<string name="force_keyframes_summary">Slower process, but more accurate cuts</string>
|
||||
<string name="use_extra_commands">Add Extra Commands</string>
|
||||
<string name="use_extra_commands_summary">For Audio / Video downloads. Add extra commands along with the GUI configuration</string>
|
||||
<string name="current">Current Command</string>
|
||||
<string name="subtitle_format">Subtitle Format\n</string>
|
||||
<string name="hide_thumbnails">Hide Thumbnails</string>
|
||||
<string name="hide_terminal">Hide Terminal from share menu</string>
|
||||
<string name="saved">Saved</string>
|
||||
<string name="clear_saved">Clear Saved</string>
|
||||
<string name="save_for_later">Save Download for Later?</string>
|
||||
</resources>
|
||||
|
|
@ -55,4 +55,8 @@
|
|||
<item name="maxLines">2</item>
|
||||
<item name="animationMode">slide</item>
|
||||
</style>
|
||||
|
||||
<style name="FullScreenDialogTheme" parent="MaterialAlertDialog.Material3">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@
|
|||
app:summary="@string/download_over_metered_networks_summary"
|
||||
app:title="@string/download_over_metered_networks" />
|
||||
|
||||
<MultiSelectListPreference
|
||||
app:icon="@drawable/baseline_hide_image_24"
|
||||
app:dialogTitle="@string/hide_thumbnails"
|
||||
app:entries="@array/hide_thumbnail"
|
||||
app:entryValues="@array/hide_thumbnail_values"
|
||||
app:key="hide_thumbnails"
|
||||
app:title="@string/hide_thumbnails" />
|
||||
|
||||
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/baseline_network_locked_24"
|
||||
app:key="proxy"
|
||||
|
|
@ -52,13 +61,6 @@
|
|||
app:summary="@string/preferred_download_type_summary"
|
||||
app:title="@string/preferred_download_type" />
|
||||
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_key"
|
||||
app:key="api_key"
|
||||
app:defaultValue=""
|
||||
app:summary="@string/api_key_summary"
|
||||
app:title="@string/api_key" />
|
||||
|
||||
<SeekBarPreference
|
||||
app:dependency="aria2"
|
||||
android:defaultValue="3"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@
|
|||
app:summary="@string/pure_theme_summary"
|
||||
app:title="@string/high_contrast" />
|
||||
|
||||
|
||||
<MultiSelectListPreference
|
||||
app:icon="@drawable/baseline_hide_image_24"
|
||||
app:dialogTitle="@string/hide_thumbnails"
|
||||
app:entries="@array/hide_thumbnail"
|
||||
app:entryValues="@array/hide_thumbnail_values"
|
||||
app:key="hide_thumbnails"
|
||||
app:title="@string/hide_thumbnails" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/misc">
|
||||
|
|
@ -93,5 +103,12 @@
|
|||
android:key="swipe_gestures"
|
||||
app:summary="@string/swipe_gestures_summary"
|
||||
app:title="@string/swipe_gestures" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
android:icon="@drawable/baseline_share_24"
|
||||
android:key="hide_terminal"
|
||||
app:title="@string/hide_terminal" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
|
@ -68,6 +68,15 @@
|
|||
app:summary="@string/force_keyframes_summary"
|
||||
app:title="@string/force_keyframes" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="srt"
|
||||
android:entries="@array/sub_formats"
|
||||
android:entryValues="@array/sub_formats"
|
||||
android:icon="@drawable/baseline_format_quote_24"
|
||||
app:key="sub_format"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:title="@string/subtitle_format" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="true"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
<EditTextPreference
|
||||
app:key="piped_instance"
|
||||
app:defaultValue="https://pipedapi.syncpundit.io"
|
||||
app:defaultValue="https://api.piped.projectsegfau.lt"
|
||||
android:summary="@string/piped_instance_summary"
|
||||
app:title="@string/piped_instance" />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue