other stuff
- formats auto updating as soon as the download card opens if auto-update is on - added preferred audio format always in the video tab - made app post downloads for queue in chunks - made app always save logs in case it fails, and if succeeds and logs are off it deletes it - fixed app navigating to home screen when cancelling download card in history screen - added a button to skip incoming app update so it wont bother you anymore - fixed settings not restoring some fields - fixed crunchyroll not working with cookies - added search for command templates - added sort filtering for command templates - added all shortcuts inside filename template creation dialog. Long click them to see the explanation - added preference to hide elements from the download card - made avc1 and m4a as preferred codecs for noobs - create dialog when download item exists. U can edit the download item right from there or access the history item to view the file
This commit is contained in:
parent
b636bf199c
commit
9353e08ee9
60 changed files with 1998 additions and 1100 deletions
|
|
@ -10,7 +10,7 @@ def properties = new Properties()
|
|||
def versionMajor = 1
|
||||
def versionMinor = 6
|
||||
def versionPatch = 8
|
||||
def versionBuild = 2 // bump for dogfood builds, public betas, etc.
|
||||
def versionBuild = 3 // bump for dogfood builds, public betas, etc.
|
||||
def versionExt = ""
|
||||
|
||||
if (versionBuild > 0){
|
||||
|
|
@ -134,7 +134,7 @@ dependencies {
|
|||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navVer"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navVer"
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||
implementation 'androidx.compose.ui:ui-android:1.5.3'
|
||||
testImplementation "junit:junit:$junitVer"
|
||||
|
|
@ -157,10 +157,10 @@ dependencies {
|
|||
implementation "androidx.room:room-runtime:2.5.2"
|
||||
implementation "androidx.room:room-ktx:2.5.2"
|
||||
ksp "androidx.room:room-compiler:2.5.2"
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.0'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation "androidx.room:room-paging:2.5.2"
|
||||
androidTestImplementation "androidx.room:room-testing:2.5.2"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
||||
implementation "androidx.compose.runtime:runtime:$composeVer"
|
||||
androidTestImplementation 'com.google.truth:truth:1.1.5'
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ dependencies {
|
|||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
|
||||
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
|
||||
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
|
||||
|
||||
// For media playback using ExoPlayer
|
||||
|
|
@ -188,5 +188,5 @@ dependencies {
|
|||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
|
||||
implementation 'com.google.accompanist:accompanist-webview:0.30.1'
|
||||
implementation 'androidx.compose.material3:material3-android:1.2.0-alpha08'
|
||||
implementation 'androidx.compose.material3:material3-android:1.2.0-alpha09'
|
||||
}
|
||||
|
|
|
|||
15
app/proguard-rules.pro
vendored
15
app/proguard-rules.pro
vendored
|
|
@ -76,4 +76,17 @@
|
|||
-dontwarn com.google.android.exoplayer2.**
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
-keep class com.deniscerri.** { *; }
|
||||
-keep class org.apache.commons.compress.archivers.zip.** { *; }
|
||||
|
||||
# Keep `Companion` object fields of serializable classes.
|
||||
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||
-if @kotlinx.serialization.Serializable class **
|
||||
-keepclassmembers class <1> {
|
||||
static <1>$Companion Companion;
|
||||
}
|
||||
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
-dontobfuscate
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
|
@ -212,16 +213,6 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.ErrorDialogActivity"
|
||||
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/Theme.BottomSheet">
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
|||
import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.ThemeUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ abstract class DBManager : RoomDatabase(){
|
|||
abstract val logDao: LogDao
|
||||
abstract val terminalDao: TerminalDao
|
||||
|
||||
enum class SORTING{
|
||||
DESC, ASC
|
||||
}
|
||||
|
||||
companion object {
|
||||
//prevents multiple instances of db getting created at the same time
|
||||
@Volatile
|
||||
|
|
|
|||
|
|
@ -2,11 +2,31 @@ package com.deniscerri.ytdlnis.database.dao
|
|||
|
||||
import androidx.room.*
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CommandTemplateDao {
|
||||
@Query("SELECT * FROM commandTemplates WHERE (title LIKE '%'||:query||'%' OR content LIKE '%'||:query||'%') ORDER BY " +
|
||||
"CASE WHEN :sort = 'ASC' THEN id END ASC," +
|
||||
"CASE WHEN :sort = 'DESC' THEN id END DESC," +
|
||||
"CASE WHEN :sort = '' THEN id END DESC ")
|
||||
fun getCommandsSortedByID(query : String, sort : String) : List<CommandTemplate>
|
||||
|
||||
|
||||
@Query("SELECT * FROM commandTemplates WHERE (title LIKE '%'||:query||'%' OR content LIKE '%'||:query||'%') ORDER BY " +
|
||||
"CASE WHEN :sort = 'ASC' THEN title COLLATE NOCASE END ASC," +
|
||||
"CASE WHEN :sort = 'DESC' THEN title COLLATE NOCASE END DESC," +
|
||||
"CASE WHEN :sort = '' THEN title COLLATE NOCASE END DESC ")
|
||||
fun getCommandsSortedByTitle(query : String, sort : String) : List<CommandTemplate>
|
||||
|
||||
@Query("SELECT * FROM commandTemplates WHERE (title LIKE '%'||:query||'%' OR content LIKE '%'||:query||'%') ORDER BY " +
|
||||
"CASE WHEN :sort = 'ASC' THEN length(content) END ASC," +
|
||||
"CASE WHEN :sort = 'DESC' THEN length(content) END DESC," +
|
||||
"CASE WHEN :sort = '' THEN length(content) END DESC ")
|
||||
fun getCommandsSortedByContentLength(query : String, sort : String) : List<CommandTemplate>
|
||||
|
||||
@Query("SELECT * FROM commandTemplates ORDER BY id DESC")
|
||||
fun getAllTemplates() : List<CommandTemplate>
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ interface HistoryDao {
|
|||
fun getAllHistoryList() : List<HistoryItem>
|
||||
|
||||
@Query("SELECT * FROM history WHERE id=:id LIMIT 1")
|
||||
suspend fun getHistoryItem(id: Int) : HistoryItem
|
||||
fun getHistoryItem(id: Long) : HistoryItem
|
||||
|
||||
@Query("SELECT * FROM history WHERE url=:url")
|
||||
fun getAllHistoryByURL(url: String) : List<HistoryItem>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.deniscerri.ytdlnis.database.repository
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -10,10 +12,22 @@ class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
|
|||
val items : Flow<List<CommandTemplate>> = commandDao.getAllTemplatesFlow()
|
||||
val shortcuts : Flow<List<TemplateShortcut>> = commandDao.getAllShortcutsFlow()
|
||||
|
||||
enum class CommandTemplateSortType {
|
||||
DATE, TITLE, LENGTH
|
||||
}
|
||||
|
||||
fun getAll() : List<CommandTemplate> {
|
||||
return commandDao.getAllTemplates()
|
||||
}
|
||||
|
||||
fun getFiltered(query : String, sortType: CommandTemplateSortType, sort: DBManager.SORTING) : List<CommandTemplate> {
|
||||
return when(sortType){
|
||||
CommandTemplateSortType.DATE -> commandDao.getCommandsSortedByID(query, sort.toString())
|
||||
CommandTemplateSortType.TITLE -> commandDao.getCommandsSortedByTitle(query, sort.toString())
|
||||
CommandTemplateSortType.LENGTH -> commandDao.getCommandsSortedByContentLength(query, sort.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun getTotalNumber() : Int {
|
||||
return commandDao.getTotalNumber()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
package com.deniscerri.ytdlnis.database.repository
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.deniscerri.ytdlnis.database.dao.HistoryDao
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import com.deniscerri.ytdlnis.database.DBManager.SORTING
|
||||
|
||||
class HistoryRepository(private val historyDao: HistoryDao) {
|
||||
val items : Flow<List<HistoryItem>> = historyDao.getAllHistory()
|
||||
enum class HistorySort{
|
||||
DESC, ASC
|
||||
}
|
||||
|
||||
enum class HistorySortType {
|
||||
DATE, TITLE, AUTHOR, FILESIZE
|
||||
}
|
||||
|
||||
suspend fun getItem(id: Int) : HistoryItem {
|
||||
fun getItem(id: Long) : HistoryItem {
|
||||
return historyDao.getHistoryItem(id)
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +22,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
|
|||
return historyDao.getAllHistoryList()
|
||||
}
|
||||
|
||||
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: HistorySort) : List<HistoryItem> {
|
||||
fun getAllByURL(url: String) : List<HistoryItem> {
|
||||
return historyDao.getAllHistoryByURL(url)
|
||||
}
|
||||
|
||||
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) : List<HistoryItem> {
|
||||
return when(sortType){
|
||||
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
|
||||
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
|
||||
|
|
@ -33,8 +34,8 @@ class HistoryRepository(private val historyDao: HistoryDao) {
|
|||
HistorySortType.FILESIZE -> {
|
||||
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString())
|
||||
when(sort){
|
||||
HistorySort.DESC -> items.sortedByDescending { it.format.filesize }
|
||||
HistorySort.ASC -> items.sortedBy { it.format.filesize }
|
||||
SORTING.DESC -> items.sortedByDescending { it.format.filesize }
|
||||
SORTING.ASC -> items.sortedBy { it.format.filesize }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,8 +45,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
|
|||
historyDao.insert(item)
|
||||
}
|
||||
|
||||
suspend fun delete(item: HistoryItem){
|
||||
suspend fun delete(item: HistoryItem, deleteFile: Boolean){
|
||||
historyDao.delete(item.id)
|
||||
if (deleteFile){
|
||||
FileUtil.deleteFile(item.downloadPath)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAll(){
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
return res
|
||||
}
|
||||
|
||||
suspend fun getOne(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
|
||||
suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
|
||||
val infoUtil = InfoUtil(context)
|
||||
val v = infoUtil.getYoutubeVideo(inputQuery)
|
||||
if (resetResults) {
|
||||
|
|
@ -81,22 +81,17 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
|
||||
suspend fun getDefault(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?> {
|
||||
val infoUtil = InfoUtil(context)
|
||||
try {
|
||||
val items = infoUtil.getFromYTDL(inputQuery)
|
||||
if (resetResults) {
|
||||
deleteAll()
|
||||
itemCount.value = items.size
|
||||
}else{
|
||||
items.forEach { it!!.playlistTitle = "ytdlnis-Search" }
|
||||
}
|
||||
items.forEach {
|
||||
resultDao.insert(it!!)
|
||||
}
|
||||
return items
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, e.toString())
|
||||
val items = infoUtil.getFromYTDL(inputQuery)
|
||||
if (resetResults) {
|
||||
deleteAll()
|
||||
itemCount.value = items.size
|
||||
}else{
|
||||
items.forEach { it!!.playlistTitle = "ytdlnis-Search" }
|
||||
}
|
||||
return arrayListOf()
|
||||
items.forEach {
|
||||
resultDao.insert(it!!)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
suspend fun delete(item: ResultItem){
|
||||
|
|
|
|||
|
|
@ -5,13 +5,17 @@ import android.content.ClipboardManager
|
|||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
||||
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -21,15 +25,55 @@ import kotlinx.serialization.json.Json
|
|||
|
||||
class CommandTemplateViewModel(private val application: Application) : AndroidViewModel(application) {
|
||||
private val repository: CommandTemplateRepository
|
||||
val items: LiveData<List<CommandTemplate>>
|
||||
val sortOrder = MutableLiveData(DBManager.SORTING.DESC)
|
||||
val sortType = MutableLiveData(CommandTemplateRepository.CommandTemplateSortType.DATE)
|
||||
private val queryFilter = MutableLiveData("")
|
||||
private var _items = MediatorLiveData<List<CommandTemplate>>()
|
||||
val allItems: LiveData<List<CommandTemplate>>
|
||||
|
||||
val shortcuts : LiveData<List<TemplateShortcut>>
|
||||
private val jsonFormat = Json { prettyPrint = true }
|
||||
|
||||
init {
|
||||
val dao = DBManager.getInstance(application).commandTemplateDao
|
||||
repository = CommandTemplateRepository(dao)
|
||||
items = repository.items.asLiveData()
|
||||
allItems = repository.items.asLiveData()
|
||||
shortcuts = repository.shortcuts.asLiveData()
|
||||
|
||||
_items.addSource(allItems){
|
||||
filter(queryFilter.value!!, sortType.value!!, sortOrder.value!!)
|
||||
}
|
||||
|
||||
_items.addSource(sortType){
|
||||
filter(queryFilter.value!!, sortType.value!!, sortOrder.value!!)
|
||||
}
|
||||
|
||||
_items.addSource(queryFilter){
|
||||
filter(queryFilter.value!!, sortType.value!!, sortOrder.value!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFilteredList() : LiveData<List<CommandTemplate>>{
|
||||
return _items
|
||||
}
|
||||
|
||||
fun setSorting(sort: CommandTemplateRepository.CommandTemplateSortType){
|
||||
if (sortType.value != sort){
|
||||
sortOrder.value = DBManager.SORTING.DESC
|
||||
}else{
|
||||
sortOrder.value = if (sortOrder.value == DBManager.SORTING.DESC) {
|
||||
DBManager.SORTING.ASC
|
||||
} else DBManager.SORTING.DESC
|
||||
}
|
||||
sortType.value = sort
|
||||
}
|
||||
|
||||
private fun filter(query : String, sortType: CommandTemplateRepository.CommandTemplateSortType, sort: DBManager.SORTING) = viewModelScope.launch(Dispatchers.IO){
|
||||
_items.postValue(repository.getFiltered(query, sortType, sort))
|
||||
}
|
||||
|
||||
fun setQueryFilter(filter: String){
|
||||
queryFilter.value = filter
|
||||
}
|
||||
|
||||
fun getTemplate(itemId: Long): CommandTemplate {
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ package com.deniscerri.ytdlnis.database.viewmodel
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
|
|
@ -40,7 +40,7 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
|
|||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
import com.deniscerri.ytdlnis.database.models.VideoPreferences
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.work.AlarmScheduler
|
||||
|
|
@ -49,8 +49,11 @@ import com.google.gson.Gson
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -85,7 +88,22 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
private var videoCodec: String?
|
||||
private var audioCodec: String?
|
||||
private val dao: DownloadDao
|
||||
private val historyDao: HistoryDao
|
||||
private val historyRepository: HistoryRepository
|
||||
|
||||
data class DownloadsUiState(
|
||||
var errorMessage: Pair<Int, Int>?,
|
||||
//action title, type, data
|
||||
var actions: MutableList<Triple<Int, DownloadsAction, List<DownloadItem>?>>?
|
||||
)
|
||||
|
||||
enum class DownloadsAction {
|
||||
DOWNLOAD_ANYWAY
|
||||
}
|
||||
|
||||
val uiState: MutableStateFlow<DownloadsUiState> = MutableStateFlow(DownloadsUiState(
|
||||
errorMessage = null,
|
||||
actions = null
|
||||
))
|
||||
|
||||
enum class Type {
|
||||
auto, audio, video, command
|
||||
|
|
@ -100,8 +118,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
init {
|
||||
dbManager = DBManager.getInstance(application)
|
||||
dao = dbManager.downloadDao
|
||||
historyDao = dbManager.historyDao
|
||||
repository = DownloadRepository(dao)
|
||||
historyRepository = HistoryRepository(dbManager.historyDao)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
|
||||
infoUtil = InfoUtil(application)
|
||||
|
|
@ -178,6 +196,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
return repository.getItemByID(id)
|
||||
}
|
||||
|
||||
fun getHistoryItemById(id: Long) : HistoryItem? {
|
||||
return historyRepository.getItem(id)
|
||||
}
|
||||
|
||||
fun getDownloadType(t: Type? = null, url: String) : Type {
|
||||
var type = t
|
||||
|
||||
|
|
@ -249,6 +271,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
}
|
||||
}
|
||||
|
||||
if (preferredAudioFormats.isEmpty()){
|
||||
preferredAudioFormats.add(getFormat(resultItem.formats, Type.audio).format_id)
|
||||
}
|
||||
|
||||
val videoPreferences = VideoPreferences(
|
||||
embedSubs,
|
||||
addChapters, false,
|
||||
|
|
@ -565,10 +591,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
startDownloadWorker(emptyList())
|
||||
}
|
||||
|
||||
suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
|
||||
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicate : Boolean = false) {
|
||||
val context = App.instance
|
||||
val alarmScheduler = AlarmScheduler(context)
|
||||
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
|
||||
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
|
||||
repository.getActiveAndQueuedDownloads()
|
||||
}
|
||||
val queuedItems = mutableListOf<DownloadItem>()
|
||||
val existing = mutableListOf<DownloadItem>()
|
||||
|
||||
|
|
@ -578,30 +606,43 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
alarmScheduler.schedule()
|
||||
}
|
||||
|
||||
|
||||
items.forEach {
|
||||
if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString()
|
||||
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
|
||||
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
|
||||
if (activeAndQueuedDownloads.firstOrNull{d ->
|
||||
d.id = 0
|
||||
d.logID = null
|
||||
d.customFileNameTemplate = it.customFileNameTemplate
|
||||
d.status = DownloadRepository.Status.Queued.toString()
|
||||
d.toString() == it.toString()
|
||||
} != null) {
|
||||
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
|
||||
d.id = 0
|
||||
d.logID = null
|
||||
d.customFileNameTemplate = it.customFileNameTemplate
|
||||
d.status = DownloadRepository.Status.Queued.toString()
|
||||
d.toString() == it.toString()
|
||||
}
|
||||
if (existingDownload != null && !ignoreDuplicate) {
|
||||
it.id = existingDownload.id
|
||||
existing.add(it)
|
||||
}else{
|
||||
//check if downloaded and file exists
|
||||
val history = historyDao.getAllHistoryByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) }
|
||||
if (history.any { h -> h.command.replace("-P \"(.*?)\"".toRegex(), "") == parsedCurrentCommand.replace("-P \"(.*?)\"".toRegex(), "") }){
|
||||
val history = withContext(Dispatchers.IO){
|
||||
historyRepository.getAllByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) }
|
||||
}
|
||||
|
||||
val existingHistory = history.firstOrNull {
|
||||
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
|
||||
}
|
||||
|
||||
if (existingHistory != null && !ignoreDuplicate){
|
||||
it.id = existingHistory.id
|
||||
existing.add(it)
|
||||
}else{
|
||||
if (it.id == 0L){
|
||||
val id = runBlocking {repository.insert(it)}
|
||||
val id = withContext(Dispatchers.IO){
|
||||
repository.insert(it)
|
||||
}
|
||||
it.id = id
|
||||
}else if (it.status == DownloadRepository.Status.Queued.toString()){
|
||||
repository.update(it)
|
||||
withContext(Dispatchers.IO){
|
||||
repository.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
queuedItems.add(it)
|
||||
|
|
@ -610,13 +651,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
}
|
||||
|
||||
if (existing.isNotEmpty()){
|
||||
val intent = Intent(context, ErrorDialogActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.putExtra("title", context.resources.getString(R.string.download_already_exists))
|
||||
intent.putExtra("message",
|
||||
"${existing.size}/${items.size}\n" +
|
||||
existing.mapIndexed { index, downloadItem -> "${index+1}. ${downloadItem.title}" }.joinToString("\n"))
|
||||
context.startActivity(intent)
|
||||
uiState.update { u -> u.copy(
|
||||
errorMessage = Pair(R.string.download_already_exists, R.string.download_already_exists_summary),
|
||||
actions = mutableListOf(
|
||||
Triple(R.string.download, DownloadsAction.DOWNLOAD_ANYWAY, existing),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || items.any { it.downloadStartTime > 0L } ){
|
||||
|
|
@ -664,10 +705,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
.setConstraints(workConstraints.build())
|
||||
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
|
||||
|
||||
if (delay > 1000L){
|
||||
queuedItems.forEach {
|
||||
workRequest.addTag(it.id.toString())
|
||||
}
|
||||
queuedItems.forEach {
|
||||
workRequest.addTag(it.id.toString())
|
||||
}
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort
|
||||
import com.deniscerri.ytdlnis.database.DBManager.SORTING
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySortType
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class HistoryViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val repository : HistoryRepository
|
||||
val sortOrder = MutableLiveData(HistorySort.DESC)
|
||||
val sortOrder = MutableLiveData(SORTING.DESC)
|
||||
val sortType = MutableLiveData(HistorySortType.DATE)
|
||||
val websiteFilter = MutableLiveData("")
|
||||
private val queryFilter = MutableLiveData("")
|
||||
|
|
@ -55,11 +55,11 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
fun setSorting(sort: HistorySortType){
|
||||
if (sortType.value != sort){
|
||||
sortOrder.value = HistorySort.DESC
|
||||
sortOrder.value = SORTING.DESC
|
||||
}else{
|
||||
sortOrder.value = if (sortOrder.value == HistorySort.DESC) {
|
||||
HistorySort.ASC
|
||||
} else HistorySort.DESC
|
||||
sortOrder.value = if (sortOrder.value == SORTING.DESC) {
|
||||
SORTING.ASC
|
||||
} else SORTING.DESC
|
||||
}
|
||||
sortType.value = sort
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
|
|||
formatFilter.value = filter
|
||||
}
|
||||
|
||||
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: HistorySort) = viewModelScope.launch(Dispatchers.IO){
|
||||
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) = viewModelScope.launch(Dispatchers.IO){
|
||||
_items.postValue(repository.getFiltered(query, format, site, sortType, sort))
|
||||
}
|
||||
|
||||
|
|
@ -89,10 +89,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
|
||||
fun delete(item: HistoryItem, deleteFile: Boolean) = viewModelScope.launch(Dispatchers.IO){
|
||||
repository.delete(item)
|
||||
if (deleteFile){
|
||||
FileUtil.deleteFile(item.downloadPath)
|
||||
}
|
||||
repository.delete(item, deleteFile)
|
||||
}
|
||||
|
||||
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
|
|||
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
|
||||
import com.deniscerri.ytdlnis.database.repository.ResultRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.regex.Pattern
|
||||
|
|
@ -27,19 +31,28 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
|
|||
val repository : ResultRepository
|
||||
private val searchHistoryRepository : SearchHistoryRepository
|
||||
val items : LiveData<List<ResultItem>>
|
||||
val loadingItems = MutableLiveData<Boolean>()
|
||||
private val sharedPreferences: SharedPreferences
|
||||
var state: ResultsState = ResultsState.IDLE
|
||||
enum class ResultsState {
|
||||
PROCESSING, IDLE
|
||||
data class ResultsUiState(
|
||||
var processing: Boolean,
|
||||
var errorMessage: Pair<Int, String>?,
|
||||
var actions: MutableList<Pair<Int, ResultAction>>?
|
||||
)
|
||||
|
||||
enum class ResultAction {
|
||||
COPY_LOG
|
||||
}
|
||||
|
||||
val uiState: MutableStateFlow<ResultsUiState> = MutableStateFlow(ResultsUiState(
|
||||
processing = false,
|
||||
errorMessage = null,
|
||||
actions = null
|
||||
))
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
val dao = DBManager.getInstance(application).resultDao
|
||||
repository = ResultRepository(dao, getApplication<Application>().applicationContext)
|
||||
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
|
||||
items = repository.allResults.asLiveData()
|
||||
loadingItems.postValue(false)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
}
|
||||
|
||||
|
|
@ -64,54 +77,38 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
|
|||
deleteAll()
|
||||
}
|
||||
}
|
||||
suspend fun parseQueries(inputQueries: List<String>) : MutableList<ResultItem?>{
|
||||
state = ResultsState.PROCESSING
|
||||
return if (inputQueries.size == 1){
|
||||
parseQuery(inputQueries[0], true)
|
||||
}else {
|
||||
suspend fun parseQueries(inputQueries: List<String>) : List<ResultItem?> {
|
||||
if (inputQueries.size > 1){
|
||||
repository.itemCount.value = inputQueries.size
|
||||
loadingItems.postValue(true)
|
||||
val items = mutableListOf<ResultItem?>()
|
||||
inputQueries.forEach {
|
||||
items.addAll(parseQuery(it, false))
|
||||
}
|
||||
state = ResultsState.IDLE
|
||||
loadingItems.postValue(false)
|
||||
items
|
||||
}
|
||||
}
|
||||
val resetResults = inputQueries.size == 1
|
||||
|
||||
suspend fun parseQuery(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?> {
|
||||
if (resetResults)
|
||||
loadingItems.postValue(true)
|
||||
val type = getQueryType(inputQuery)
|
||||
var res = arrayListOf<ResultItem?>()
|
||||
uiState.update {it.copy(processing = true, errorMessage = null, actions = null)}
|
||||
return withContext(Dispatchers.IO){
|
||||
try {
|
||||
when (type) {
|
||||
"Search" -> {
|
||||
res = repository.search(inputQuery, resetResults)
|
||||
}
|
||||
"YT_Video" -> {
|
||||
res = repository.getOne(inputQuery, resetResults)
|
||||
}
|
||||
"YT_Playlist" -> {
|
||||
res = repository.getPlaylist(inputQuery, resetResults)
|
||||
}
|
||||
"Default" -> {
|
||||
res = repository.getDefault(inputQuery, resetResults)
|
||||
val res = mutableListOf<ResultItem?>()
|
||||
inputQueries.forEach { inputQuery ->
|
||||
val type = getQueryType(inputQuery)
|
||||
try {
|
||||
when (type) {
|
||||
"Search" -> res.addAll(repository.search(inputQuery, resetResults))
|
||||
"YT_Video" -> res.addAll(repository.getYoutubeVideo(inputQuery, resetResults))
|
||||
"YT_Playlist" -> res.addAll(repository.getPlaylist(inputQuery, resetResults))
|
||||
else -> res.addAll(repository.getDefault(inputQuery, resetResults))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
uiState.update {it.copy(
|
||||
processing = false,
|
||||
errorMessage = Pair(R.string.no_results, e.message.toString()),
|
||||
actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG))
|
||||
)}
|
||||
Log.e(tag, e.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, e.toString())
|
||||
}
|
||||
if (resetResults) {
|
||||
state = ResultsState.IDLE
|
||||
loadingItems.postValue(false)
|
||||
}
|
||||
uiState.update {it.copy(processing = false)}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
private fun getQueryType(inputQuery: String) : String {
|
||||
var type = "Search"
|
||||
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ import android.os.Bundle
|
|||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
|
|
@ -35,7 +32,6 @@ import com.deniscerri.ytdlnis.ui.BaseActivity
|
|||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog
|
||||
import com.deniscerri.ytdlnis.util.ThemeUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
package com.deniscerri.ytdlnis.ui
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.util.ThemeUtil
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
|
||||
open class ErrorDialogActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val title = intent.extras?.getString("title") ?: ""
|
||||
val message = intent.extras?.getString("message") ?: ""
|
||||
|
||||
ThemeUtil.updateTheme(this)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
|
||||
v.setPadding(0, 0, 0, 0)
|
||||
insets
|
||||
}
|
||||
window.run {
|
||||
setBackgroundDrawable(ColorDrawable(0))
|
||||
setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
|
||||
} else {
|
||||
setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
|
||||
}
|
||||
}
|
||||
|
||||
val errDialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setIcon(R.drawable.ic_cancel)
|
||||
.setPositiveButton(getString(R.string.copy_log)) { _: DialogInterface?, _: Int ->
|
||||
val clipboard: ClipboardManager =
|
||||
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setText(message)
|
||||
this.finish()
|
||||
}.setOnDismissListener {
|
||||
this.finish()
|
||||
}
|
||||
this.runOnUiThread {
|
||||
errDialog.show()
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ 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.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
|
||||
import com.facebook.shimmer.ShimmerFrameLayout
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
|
|
@ -54,6 +55,8 @@ import com.google.android.material.search.SearchBar
|
|||
import com.google.android.material.search.SearchView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
|
@ -105,6 +108,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
activity = getActivity()
|
||||
mainActivity = activity as MainActivity?
|
||||
quickLaunchSheet = false
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +140,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
searchSuggestionsLinearLayout = view.findViewById(R.id.search_suggestions_linear_layout)
|
||||
searchHistory = view.findViewById(R.id.search_history_scroll_view)
|
||||
searchHistoryLinearLayout = view.findViewById(R.id.search_history_linear_layout)
|
||||
homeFabs = view.findViewById(R.id.home_fabs)
|
||||
downloadFabs = homeFabs!!.findViewById(R.id.download_selected_coordinator)
|
||||
downloadAllFabCoordinator = homeFabs!!.findViewById(R.id.download_all_coordinator)
|
||||
|
||||
runCatching { materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext()) }
|
||||
|
||||
|
|
@ -174,30 +181,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
quickLaunchSheet = true
|
||||
}
|
||||
|
||||
|
||||
resultViewModel.loadingItems.observe(viewLifecycleOwner){
|
||||
loadingItems = it
|
||||
if (it){
|
||||
recyclerView?.setPadding(0,0,0,0)
|
||||
shimmerCards!!.startShimmer()
|
||||
shimmerCards!!.visibility = VISIBLE
|
||||
}else{
|
||||
recyclerView?.setPadding(0,0,0,100)
|
||||
shimmerCards!!.stopShimmer()
|
||||
shimmerCards!!.visibility = GONE
|
||||
if (resultsList!!.size > 1 && resultsList!![0]!!.playlistTitle.isNotEmpty()){
|
||||
downloadAllFabCoordinator!!.visibility = VISIBLE
|
||||
}else{
|
||||
downloadAllFabCoordinator!!.visibility = GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initMenu()
|
||||
|
||||
homeFabs = view.findViewById(R.id.home_fabs)
|
||||
downloadFabs = homeFabs!!.findViewById(R.id.download_selected_coordinator)
|
||||
downloadAllFabCoordinator = homeFabs!!.findViewById(R.id.download_all_coordinator)
|
||||
val downloadSelectedFab = downloadFabs!!.findViewById<ExtendedFloatingActionButton>(R.id.download_selected_fab)
|
||||
downloadSelectedFab.tag = "downloadSelected"
|
||||
downloadSelectedFab.setOnClickListener(this)
|
||||
|
|
@ -239,15 +223,40 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
launch{
|
||||
resultViewModel.uiState.collectLatest { res ->
|
||||
if (res.errorMessage != null){
|
||||
kotlin.runCatching { UiUtil.handleResultResponse(requireActivity(), res) }
|
||||
resultViewModel.uiState.update {it.copy(errorMessage = null, actions = null) }
|
||||
}
|
||||
|
||||
loadingItems = res.processing
|
||||
if (res.processing){
|
||||
recyclerView?.setPadding(0,0,0,0)
|
||||
shimmerCards!!.startShimmer()
|
||||
shimmerCards!!.visibility = VISIBLE
|
||||
}else{
|
||||
recyclerView?.setPadding(0,0,0,100)
|
||||
shimmerCards!!.stopShimmer()
|
||||
shimmerCards!!.visibility = GONE
|
||||
if (resultsList!!.size > 1 && resultsList!![0]!!.playlistTitle.isNotEmpty()){
|
||||
downloadAllFabCoordinator!!.visibility = VISIBLE
|
||||
}else{
|
||||
downloadAllFabCoordinator!!.visibility = GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if(arguments?.getString("url") == null){
|
||||
if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){
|
||||
if (!resultViewModel.uiState.value.processing){
|
||||
resultViewModel.checkTrending()
|
||||
}else{
|
||||
resultViewModel.loadingItems.postValue(true)
|
||||
}
|
||||
}else{
|
||||
arguments?.remove("url")
|
||||
|
|
@ -302,7 +311,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
}
|
||||
|
||||
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
searchView!!.addTransitionListener { _, _, newState ->
|
||||
if (newState == SearchView.TransitionState.SHOWN) {
|
||||
val currentProvider = sharedPreferences?.getString("search_engine", "ytsearch")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,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.UiUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil.setTextAndRecalculateWidth
|
||||
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
|
@ -496,10 +497,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
}
|
||||
|
||||
chip.setOnLongClickListener {
|
||||
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + chip.text + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
UiUtil.showGenericDeleteDialog(requireContext(), chip.text.toString(), accepted = {
|
||||
player.seekTo(0)
|
||||
player.pause()
|
||||
chipGroup.removeView(chip)
|
||||
|
|
@ -509,8 +507,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
player.stop()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
deleteDialog.show()
|
||||
})
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
|
@ -11,11 +12,13 @@ import android.view.ViewGroup
|
|||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.isEmpty
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -52,6 +55,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
lateinit var downloadItem : DownloadItem
|
||||
lateinit var title : TextInputLayout
|
||||
lateinit var author : TextInputLayout
|
||||
lateinit var preferences: SharedPreferences
|
||||
lateinit var shownFields: List<String>
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
|
@ -63,6 +68,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +89,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
|
||||
try {
|
||||
title = view.findViewById(R.id.title_textinput)
|
||||
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
|
||||
if (title.editText?.text?.isEmpty() == true){
|
||||
title.editText!!.setText(downloadItem.title)
|
||||
}
|
||||
|
|
@ -100,6 +108,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
}
|
||||
|
||||
author = view.findViewById(R.id.author_textinput)
|
||||
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
|
||||
if (author.editText?.text?.isEmpty() == true){
|
||||
author.editText!!.setText(downloadItem.author)
|
||||
}
|
||||
|
|
@ -158,9 +167,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
if (currentDownloadItem!!.type != Type.audio){
|
||||
downloadItem.type = Type.audio
|
||||
runCatching {
|
||||
downloadItem.format =
|
||||
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
|
||||
.maxByOrNull { it.filesize }!!
|
||||
downloadItem.format = downloadViewModel.getFormat(downloadItem.allFormats, Type.audio)
|
||||
}.onFailure {
|
||||
downloadItem.format = genericAudioFormats.last()
|
||||
}
|
||||
|
|
@ -172,6 +179,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
var containerPreference = sharedPreferences.getString("audio_format", "Default")
|
||||
if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue)
|
||||
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
|
||||
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
|
||||
val containerAutoCompleteTextView =
|
||||
view.findViewById<AutoCompleteTextView>(R.id.container_textview)
|
||||
|
||||
|
|
@ -190,6 +198,10 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
|
||||
if (resultItem != null){
|
||||
resultViewModel.update(resultItem!!)
|
||||
kotlin.runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
|
||||
f1.updateUI(resultItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -229,44 +241,50 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
if (containers[index] == getString(R.string.defaultValue)) downloadItem.container = ""
|
||||
}
|
||||
|
||||
UiUtil.configureAudio(
|
||||
view,
|
||||
requireActivity(),
|
||||
listOf(downloadItem),
|
||||
embedThumbClicked = {
|
||||
downloadItem.audioPreferences.embedThumb = it
|
||||
},
|
||||
splitByChaptersClicked = {
|
||||
downloadItem.audioPreferences.splitByChapters = it
|
||||
},
|
||||
filenameTemplateSet = {
|
||||
downloadItem.customFileNameTemplate = it
|
||||
},
|
||||
sponsorBlockItemsSet = { values, checkedItems ->
|
||||
downloadItem.audioPreferences.sponsorBlockFilters.clear()
|
||||
for (i in checkedItems.indices) {
|
||||
if (checkedItems[i]) {
|
||||
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
cutClicked = {cutVideoListener ->
|
||||
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
|
||||
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
|
||||
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
|
||||
}
|
||||
},
|
||||
extraCommandsClicked = {
|
||||
val callback = object : ExtraCommandsListener {
|
||||
override fun onChangeExtraCommand(c: String) {
|
||||
downloadItem.extraCommands = c
|
||||
}
|
||||
}
|
||||
|
||||
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
|
||||
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
|
||||
view.findViewById<LinearLayout>(R.id.adjust).apply {
|
||||
visibility = if (shownFields.contains("adjust_audio")) View.VISIBLE else View.GONE
|
||||
if (isVisible){
|
||||
UiUtil.configureAudio(
|
||||
view,
|
||||
requireActivity(),
|
||||
listOf(downloadItem),
|
||||
embedThumbClicked = {
|
||||
downloadItem.audioPreferences.embedThumb = it
|
||||
},
|
||||
splitByChaptersClicked = {
|
||||
downloadItem.audioPreferences.splitByChapters = it
|
||||
},
|
||||
filenameTemplateSet = {
|
||||
downloadItem.customFileNameTemplate = it
|
||||
},
|
||||
sponsorBlockItemsSet = { values, checkedItems ->
|
||||
downloadItem.audioPreferences.sponsorBlockFilters.clear()
|
||||
for (i in checkedItems.indices) {
|
||||
if (checkedItems[i]) {
|
||||
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
cutClicked = {cutVideoListener ->
|
||||
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
|
||||
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
|
||||
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
|
||||
}
|
||||
},
|
||||
extraCommandsClicked = {
|
||||
val callback = object : ExtraCommandsListener {
|
||||
override fun onChangeExtraCommand(c: String) {
|
||||
downloadItem.extraCommands = c
|
||||
}
|
||||
}
|
||||
|
||||
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
|
||||
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}catch (e : Exception){
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package com.deniscerri.ytdlnis.ui.downloadcard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
|
|
@ -20,14 +17,12 @@ import android.widget.Button
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.deniscerri.ytdlnis.MainActivity
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
|
||||
|
|
@ -45,9 +40,10 @@ 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.snackbar.Snackbar
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -205,6 +201,11 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
viewPager2.setPageTransformer(BackgroundToForegroundPageTransformer())
|
||||
|
||||
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
|
||||
scheduleBtn.visibility = if(sharedPreferences.getStringSet("modify_download_card", setOf())!!.contains("schedule")){
|
||||
View.VISIBLE
|
||||
}else{
|
||||
View.GONE
|
||||
}
|
||||
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
|
||||
|
||||
|
||||
|
|
@ -249,6 +250,11 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
}
|
||||
|
||||
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
link.visibility = if(sharedPreferences.getStringSet("modify_download_card", setOf())!!.contains("url")){
|
||||
View.VISIBLE
|
||||
}else{
|
||||
View.GONE
|
||||
}
|
||||
val updateItem = view.findViewById<Button>(R.id.update_item)
|
||||
if (Patterns.WEB_URL.matcher(result.url).matches()){
|
||||
link.text = result.url
|
||||
|
|
@ -282,6 +288,23 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
//update in the background if there is no data
|
||||
if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false)){
|
||||
initUpdateData(view)
|
||||
}else {
|
||||
val usingGenericFormatsOrEmpty = result.formats.isEmpty() || result.formats.any { it.format_note.contains("ytdlnisgeneric") }
|
||||
if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false)){
|
||||
initUpdateFormats(result.url)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.uiState.collectLatest { res ->
|
||||
if (res.errorMessage != null) {
|
||||
UiUtil.handleDownloadsResponse(requireActivity() as MainActivity, res, downloadViewModel)
|
||||
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
|
||||
errorMessage = null,
|
||||
actions = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
|
||||
|
|
@ -342,7 +365,7 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
return@launch
|
||||
}
|
||||
|
||||
val result = resultViewModel.parseQuery(result.url, true)
|
||||
val result = resultViewModel.parseQueries(listOf(result.url))
|
||||
if (result.isEmpty()){
|
||||
withContext(Dispatchers.Main){
|
||||
Looper.prepare().run {
|
||||
|
|
@ -376,6 +399,12 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
shimmerLoadingSubtitle.stopShimmer()
|
||||
}
|
||||
|
||||
val usingGenericFormatsOrEmpty = result[0]!!.formats.isEmpty() || result[0]!!.formats.any { it.format_note.contains("ytdlnisgeneric") }
|
||||
|
||||
if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false)){
|
||||
initUpdateFormats(result[0]!!.url)
|
||||
}
|
||||
|
||||
}else{
|
||||
//open multi download card instead
|
||||
if (activity is ShareActivity){
|
||||
|
|
@ -394,6 +423,48 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
}
|
||||
}
|
||||
|
||||
private fun initUpdateFormats(url: String){
|
||||
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main){
|
||||
runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
|
||||
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE
|
||||
}
|
||||
runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
|
||||
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
val formats = infoUtil.getFormats(url).toMutableList()
|
||||
|
||||
withContext(Dispatchers.Main){
|
||||
runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
|
||||
val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem)
|
||||
resultItem.formats = formats
|
||||
fragmentAdapter.setResultItem(resultItem)
|
||||
f1.updateUI(resultItem)
|
||||
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
|
||||
}
|
||||
runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
|
||||
val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem)
|
||||
resultItem.formats = formats
|
||||
fragmentAdapter.setResultItem(resultItem)
|
||||
f1.updateUI(resultItem)
|
||||
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (formats.isNotEmpty()){
|
||||
result.formats = formats
|
||||
resultViewModel.update(result)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
cleanUp()
|
||||
|
|
@ -407,7 +478,7 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
|
|||
|
||||
private fun cleanUp(){
|
||||
kotlin.runCatching {
|
||||
repeat(parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount ?: 0){
|
||||
repeat((parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount?.minus(1))?: 0){
|
||||
parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.popBackStack()
|
||||
}
|
||||
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.app.Activity
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
|
@ -17,9 +18,11 @@ import android.widget.*
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
|
|
@ -48,6 +51,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
|
|||
private lateinit var commandTemplateViewModel : CommandTemplateViewModel
|
||||
private lateinit var saveDir : TextInputLayout
|
||||
private lateinit var freeSpace : TextView
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private lateinit var shownFields: List<String>
|
||||
|
||||
lateinit var downloadItem: DownloadItem
|
||||
|
||||
|
|
@ -60,6 +65,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
|
|||
activity = getActivity()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
|
|
@ -180,72 +187,76 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
|
|||
commandTemplateViewModel.getTotalShortcutNumber()
|
||||
}
|
||||
|
||||
UiUtil.configureCommand(
|
||||
view,
|
||||
if (downloadItem.url.isEmpty()) 0 else 1,
|
||||
shortcutCount,
|
||||
newTemplateClicked = {
|
||||
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
templates.add(it)
|
||||
chosenCommandView.editText!!.setText(it.content)
|
||||
downloadItem.format = Format(
|
||||
it.title,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
it.content
|
||||
)
|
||||
commandTemplateCard.visibility = View.VISIBLE
|
||||
view.findViewById<TextView>(R.id.command_txt).visibility = View.VISIBLE
|
||||
view.findViewById<Chip>(R.id.editSelected).isEnabled = true
|
||||
populateCommandCard(commandTemplateCard, it)
|
||||
}
|
||||
},
|
||||
editSelectedClicked =
|
||||
{
|
||||
var current = templates.find { it.id == commandTemplateCard.tag }
|
||||
if (current == null) current =
|
||||
CommandTemplate(
|
||||
0,
|
||||
"",
|
||||
chosenCommandView.editText!!.text.toString(),
|
||||
false
|
||||
)
|
||||
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
templates.add(it)
|
||||
chosenCommandView.editText!!.setText(it.content)
|
||||
populateCommandCard(commandTemplateCard, it)
|
||||
downloadItem.format = Format(
|
||||
it.title,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
it.content
|
||||
)
|
||||
}
|
||||
},
|
||||
shortcutClicked = {
|
||||
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
|
||||
itemSelected = {
|
||||
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $it")
|
||||
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString()
|
||||
},
|
||||
itemRemoved = {removed ->
|
||||
chosenCommandView.editText!!.setText(chosenCommandView.editText!!.text.replace("(${
|
||||
Regex.escape(
|
||||
removed
|
||||
|
||||
view.findViewById<LinearLayout>(R.id.adjust).apply {
|
||||
visibility = if (shownFields.contains("adjust_command")) View.VISIBLE else View.GONE
|
||||
if (isVisible){
|
||||
UiUtil.configureCommand(
|
||||
view,
|
||||
if (downloadItem.url.isEmpty()) 0 else 1,
|
||||
shortcutCount,
|
||||
newTemplateClicked = {
|
||||
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
templates.add(it)
|
||||
chosenCommandView.editText!!.setText(it.content)
|
||||
downloadItem.format = Format(
|
||||
it.title,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
it.content
|
||||
)
|
||||
})(?!.*\\1)".toRegex(), "").trimEnd())
|
||||
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString().trimEnd()
|
||||
})
|
||||
commandTemplateCard.visibility = View.VISIBLE
|
||||
view.findViewById<TextView>(R.id.command_txt).visibility = View.VISIBLE
|
||||
view.findViewById<Chip>(R.id.editSelected).isEnabled = true
|
||||
populateCommandCard(commandTemplateCard, it)
|
||||
}
|
||||
},
|
||||
editSelectedClicked =
|
||||
{
|
||||
var current = templates.find { it.id == commandTemplateCard.tag }
|
||||
if (current == null) current =
|
||||
CommandTemplate(
|
||||
0,
|
||||
"",
|
||||
chosenCommandView.editText!!.text.toString(),
|
||||
false
|
||||
)
|
||||
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
templates.add(it)
|
||||
chosenCommandView.editText!!.setText(it.content)
|
||||
populateCommandCard(commandTemplateCard, it)
|
||||
downloadItem.format = Format(
|
||||
it.title,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
it.content
|
||||
)
|
||||
}
|
||||
},
|
||||
shortcutClicked = {
|
||||
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
|
||||
itemSelected = {
|
||||
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $it")
|
||||
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString()
|
||||
},
|
||||
itemRemoved = {removed ->
|
||||
chosenCommandView.editText!!.setText(chosenCommandView.editText!!.text.replace("(${
|
||||
Regex.escape(
|
||||
removed
|
||||
)
|
||||
})(?!.*\\1)".toRegex(), "").trimEnd())
|
||||
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString().trimEnd()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import androidx.work.Data
|
|||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdlnis.MainActivity
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
|
|
@ -55,9 +56,8 @@ 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.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -506,6 +506,18 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
|
|||
true
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.uiState.collectLatest { res ->
|
||||
if (res.errorMessage != null) {
|
||||
UiUtil.handleDownloadsResponse(requireActivity() as MainActivity, res, downloadViewModel)
|
||||
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
|
||||
errorMessage = null,
|
||||
actions = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateFileSize(items: List<Long>){
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
|
@ -13,9 +14,11 @@ import android.view.ViewGroup
|
|||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -44,7 +47,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
private var activity: Activity? = null
|
||||
private lateinit var downloadViewModel : DownloadViewModel
|
||||
private lateinit var resultViewModel: ResultViewModel
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private lateinit var shownFields: List<String>
|
||||
lateinit var title : TextInputLayout
|
||||
lateinit var author : TextInputLayout
|
||||
private lateinit var saveDir : TextInputLayout
|
||||
|
|
@ -54,6 +58,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
|
||||
lateinit var downloadItem: DownloadItem
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
|
@ -65,6 +70,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
resultViewModel = ViewModelProvider(this@DownloadVideoFragment)[ResultViewModel::class.java]
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +92,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
try {
|
||||
title = view.findViewById(R.id.title_textinput)
|
||||
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
|
||||
if (title.editText?.text?.isEmpty() == true){
|
||||
title.editText!!.setText(downloadItem.title)
|
||||
}
|
||||
|
|
@ -97,6 +105,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
})
|
||||
|
||||
author = view.findViewById(R.id.author_textinput)
|
||||
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
|
||||
if (author.editText?.text?.isEmpty() == true){
|
||||
author.editText!!.setText(downloadItem.author)
|
||||
}
|
||||
|
|
@ -149,9 +158,12 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
if (currentDownloadItem!!.type != Type.video){
|
||||
downloadItem.type = Type.video
|
||||
runCatching {
|
||||
downloadItem.format =
|
||||
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
|
||||
.maxByOrNull { it.filesize }!!
|
||||
downloadItem.format = downloadViewModel.getFormat(downloadItem.allFormats, Type.video)
|
||||
if (downloadItem.videoPreferences.audioFormatIDs.isEmpty()){
|
||||
downloadItem.videoPreferences.audioFormatIDs.add(
|
||||
downloadViewModel.getFormat(downloadItem.allFormats, Type.audio).format_id
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
downloadItem.format = genericVideoFormats.last()
|
||||
}
|
||||
|
|
@ -161,6 +173,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
|
||||
val containers = requireContext().resources.getStringArray(R.array.video_containers)
|
||||
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
|
||||
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
|
||||
val containerAutoCompleteTextView =
|
||||
view.findViewById<AutoCompleteTextView>(R.id.container_textview)
|
||||
var containerPreference = sharedPreferences.getString("video_format", "Default")
|
||||
|
|
@ -185,6 +198,10 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
resultItem?.formats?.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) })
|
||||
if (resultItem != null){
|
||||
resultViewModel.update(resultItem!!)
|
||||
kotlin.runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
|
||||
f1.updateUI(resultItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -226,61 +243,66 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
}
|
||||
|
||||
|
||||
UiUtil.configureVideo(
|
||||
view,
|
||||
requireActivity(),
|
||||
listOf(downloadItem),
|
||||
embedSubsClicked = {
|
||||
downloadItem.videoPreferences.embedSubs = it
|
||||
},
|
||||
addChaptersClicked = {
|
||||
downloadItem.videoPreferences.addChapters = it
|
||||
},
|
||||
splitByChaptersClicked = {
|
||||
downloadItem.videoPreferences.splitByChapters = it
|
||||
},
|
||||
saveThumbnailClicked = {
|
||||
downloadItem.SaveThumb = it
|
||||
},
|
||||
sponsorBlockItemsSet = { values, checkedItems ->
|
||||
downloadItem.videoPreferences.sponsorBlockFilters.clear()
|
||||
for (i in checkedItems.indices) {
|
||||
if (checkedItems[i]) {
|
||||
downloadItem.videoPreferences.sponsorBlockFilters.add(values[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
cutClicked = { cutVideoListener ->
|
||||
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
|
||||
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
|
||||
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
|
||||
}
|
||||
},
|
||||
filenameTemplateSet = {
|
||||
downloadItem.customFileNameTemplate = it
|
||||
},
|
||||
saveSubtitlesClicked = {
|
||||
downloadItem.videoPreferences.writeSubs = it
|
||||
},
|
||||
subtitleLanguagesClicked = {
|
||||
UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
|
||||
downloadItem.videoPreferences.subsLanguages = it
|
||||
}
|
||||
},
|
||||
removeAudioClicked = {
|
||||
downloadItem.videoPreferences.removeAudio = it
|
||||
},
|
||||
extraCommandsClicked = {
|
||||
val callback = object : ExtraCommandsListener {
|
||||
override fun onChangeExtraCommand(c: String) {
|
||||
downloadItem.extraCommands = c
|
||||
}
|
||||
}
|
||||
view.findViewById<LinearLayout>(R.id.adjust).apply {
|
||||
visibility = if (shownFields.contains("adjust_video")) View.VISIBLE else View.GONE
|
||||
if (isVisible){
|
||||
UiUtil.configureVideo(
|
||||
view,
|
||||
requireActivity(),
|
||||
listOf(downloadItem),
|
||||
embedSubsClicked = {
|
||||
downloadItem.videoPreferences.embedSubs = it
|
||||
},
|
||||
addChaptersClicked = {
|
||||
downloadItem.videoPreferences.addChapters = it
|
||||
},
|
||||
splitByChaptersClicked = {
|
||||
downloadItem.videoPreferences.splitByChapters = it
|
||||
},
|
||||
saveThumbnailClicked = {
|
||||
downloadItem.SaveThumb = it
|
||||
},
|
||||
sponsorBlockItemsSet = { values, checkedItems ->
|
||||
downloadItem.videoPreferences.sponsorBlockFilters.clear()
|
||||
for (i in checkedItems.indices) {
|
||||
if (checkedItems[i]) {
|
||||
downloadItem.videoPreferences.sponsorBlockFilters.add(values[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
cutClicked = { cutVideoListener ->
|
||||
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
|
||||
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
|
||||
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
|
||||
}
|
||||
},
|
||||
filenameTemplateSet = {
|
||||
downloadItem.customFileNameTemplate = it
|
||||
},
|
||||
saveSubtitlesClicked = {
|
||||
downloadItem.videoPreferences.writeSubs = it
|
||||
},
|
||||
subtitleLanguagesClicked = {
|
||||
UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
|
||||
downloadItem.videoPreferences.subsLanguages = it
|
||||
}
|
||||
},
|
||||
removeAudioClicked = {
|
||||
downloadItem.videoPreferences.removeAudio = it
|
||||
},
|
||||
extraCommandsClicked = {
|
||||
val callback = object : ExtraCommandsListener {
|
||||
override fun onChangeExtraCommand(c: String) {
|
||||
downloadItem.extraCommands = c
|
||||
}
|
||||
}
|
||||
|
||||
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
|
||||
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
|
||||
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
|
||||
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import android.widget.TextView
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.map
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
|
@ -28,7 +27,6 @@ import com.google.android.material.button.MaterialButton
|
|||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
|
||||
fragmentView = inflater.inflate(R.layout.generic_recyclerview, container, false)
|
||||
activity = getActivity()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
return fragmentView
|
||||
|
|
@ -100,7 +100,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
downloadViewModel.getItemByID(itemID)
|
||||
}
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
downloadViewModel.queueDownloads(listOf(item), true)
|
||||
}
|
||||
}.onFailure {
|
||||
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
|
||||
|
|
@ -264,6 +264,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val itemID = viewHolder.itemView.tag.toString().toLong()
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
when (direction) {
|
||||
ItemTouchHelper.LEFT -> {
|
||||
lifecycleScope.launch {
|
||||
|
|
@ -279,6 +280,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
}
|
||||
ItemTouchHelper.RIGHT -> {
|
||||
onActionButtonClick(itemID)
|
||||
adapter.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
|
||||
fragmentView = inflater.inflate(R.layout.generic_recyclerview, container, false)
|
||||
activity = getActivity()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
return fragmentView
|
||||
|
|
@ -269,10 +269,15 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
|
|||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val itemID = viewHolder.itemView.tag.toString().toLong()
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
when (direction) {
|
||||
ItemTouchHelper.RIGHT -> {
|
||||
runBlocking{
|
||||
downloadViewModel.reQueueDownloadItems(listOf(itemID))
|
||||
lifecycleScope.launch {
|
||||
val item = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getItemByID(itemID)
|
||||
}
|
||||
downloadViewModel.queueDownloads(listOf(item), true)
|
||||
adapter.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
ItemTouchHelper.LEFT -> {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import android.view.View.GONE
|
|||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
|
|
@ -41,28 +40,22 @@ import com.deniscerri.ytdlnis.R
|
|||
import com.deniscerri.ytdlnis.adapter.HistoryAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort
|
||||
import com.deniscerri.ytdlnis.database.DBManager.SORTING
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
|
||||
import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
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.chip.ChipGroup
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* A fragment representing a list of Items.
|
||||
|
|
@ -163,8 +156,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
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)
|
||||
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
|
||||
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -277,12 +270,12 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
}
|
||||
}
|
||||
|
||||
private fun changeSortIcon(item: TextView, order: HistorySort){
|
||||
private fun changeSortIcon(item: TextView, order: SORTING){
|
||||
when(order){
|
||||
HistorySort.DESC ->{
|
||||
SORTING.DESC ->{
|
||||
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_up, 0,0,0)
|
||||
}
|
||||
HistorySort.ASC -> {
|
||||
SORTING.ASC -> {
|
||||
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_down, 0,0,0)
|
||||
}
|
||||
}
|
||||
|
|
@ -405,146 +398,23 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
requireView().findViewById<View>(R.id.website_divider).visibility = VISIBLE
|
||||
}
|
||||
|
||||
|
||||
private fun removeItem(item: HistoryItem) {
|
||||
if (bottomSheet != null) bottomSheet!!.hide()
|
||||
val deleteFile = booleanArrayOf(false)
|
||||
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
|
||||
val path = item.downloadPath
|
||||
val file = File(path)
|
||||
if (file.exists() && path.isNotEmpty()) {
|
||||
deleteDialog.setMultiChoiceItems(
|
||||
arrayOf(getString(R.string.delete_file_too)),
|
||||
booleanArrayOf(false)
|
||||
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
|
||||
}
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
historyViewModel.delete(item, deleteFile[0])
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun onCardClick(itemID: Long, isPresent: Boolean) {
|
||||
bottomSheet = BottomSheetDialog(fragmentContext!!)
|
||||
bottomSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet!!.setContentView(R.layout.history_item_details_bottom_sheet)
|
||||
val item = findItem(itemID)
|
||||
val title = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_title)
|
||||
title!!.text = item!!.title
|
||||
val author = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet!!.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
|
||||
if (item.type == DownloadViewModel.Type.audio) {
|
||||
if (isPresent) btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music_downloaded) else btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
|
||||
} else if (item.type == DownloadViewModel.Type.video) {
|
||||
if (isPresent) btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video_downloaded) else btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
|
||||
}else{
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
|
||||
}
|
||||
|
||||
if (isPresent){
|
||||
btn.setOnClickListener {
|
||||
UiUtil!!.shareFileIntent(requireContext(), listOf(item.downloadPath))
|
||||
UiUtil.showHistoryItemDetailsCard(item, requireActivity(), isPresent,
|
||||
redownloadItem = {
|
||||
val downloadItem = downloadViewModel.createDownloadItemFromHistory(it)
|
||||
runBlocking{
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
}
|
||||
historyViewModel.delete(it, false)
|
||||
}
|
||||
}
|
||||
|
||||
val time = bottomSheet!!.findViewById<TextView>(R.id.time)
|
||||
val formatNote = bottomSheet!!.findViewById<TextView>(R.id.format_note)
|
||||
val container = bottomSheet!!.findViewById<TextView>(R.id.container_chip)
|
||||
val codec = bottomSheet!!.findViewById<TextView>(R.id.codec)
|
||||
val fileSize = bottomSheet!!.findViewById<TextView>(R.id.file_size)
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = item.time * 1000L
|
||||
time!!.text = SimpleDateFormat(android.text.format.DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
|
||||
time.isClickable = false
|
||||
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = GONE
|
||||
else formatNote!!.text = item.format.format_note
|
||||
|
||||
if (item.format.container != "") container!!.text = item.format.container.uppercase()
|
||||
else container!!.visibility = 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 = GONE
|
||||
}else{
|
||||
codec!!.visibility = VISIBLE
|
||||
codec.text = codecText
|
||||
}
|
||||
|
||||
val file = File(item.downloadPath)
|
||||
val fileSizeReadable = FileUtil.convertFileSize(if (file.exists()) file.length() else item.format.filesize)
|
||||
if (fileSizeReadable == "?") fileSize!!.visibility = 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)
|
||||
}
|
||||
val openFile = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_open_file_button)
|
||||
openFile!!.tag = itemID
|
||||
openFile.setOnClickListener{
|
||||
UiUtil.openFileIntent(requireContext(), item.downloadPath)
|
||||
}
|
||||
|
||||
val redownload = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_redownload_button)
|
||||
redownload!!.tag = itemID
|
||||
redownload.setOnClickListener{
|
||||
val downloadItem = downloadViewModel.createDownloadItemFromHistory(item)
|
||||
runBlocking{
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
}
|
||||
historyViewModel.delete(item, false)
|
||||
bottomSheet!!.cancel()
|
||||
}
|
||||
|
||||
redownload.setOnLongClickListener {
|
||||
bottomSheet!!.cancel()
|
||||
) {
|
||||
val sheet = DownloadBottomSheetDialog(
|
||||
result = downloadViewModel.createResultItemFromHistory(item),
|
||||
type = item.type)
|
||||
result = downloadViewModel.createResultItemFromHistory(it),
|
||||
type = it.type
|
||||
)
|
||||
sheet.show(parentFragmentManager, "downloadSingleSheet")
|
||||
true
|
||||
}
|
||||
|
||||
if (!isPresent) openFile.visibility = GONE
|
||||
else redownload.visibility = 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) {
|
||||
|
|
@ -666,7 +536,10 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
ItemTouchHelper.LEFT -> {
|
||||
val deletedItem = historyList!![position]
|
||||
historyAdapter!!.notifyItemChanged(position)
|
||||
removeItem(deletedItem!!)
|
||||
UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(),
|
||||
delete = { item, deleteFile ->
|
||||
historyViewModel.delete(item, deleteFile)
|
||||
})
|
||||
}
|
||||
ItemTouchHelper.RIGHT -> {
|
||||
val item = historyList!![position]!!
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
|
||||
fragmentView = inflater.inflate(R.layout.generic_recyclerview, container, false)
|
||||
activity = getActivity()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
return fragmentView
|
||||
|
|
|
|||
|
|
@ -4,11 +4,19 @@ import android.content.DialogInterface
|
|||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.InputType
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -21,9 +29,12 @@ import com.deniscerri.ytdlnis.R
|
|||
import com.deniscerri.ytdlnis.adapter.TemplatesAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository.CommandTemplateSortType
|
||||
import com.deniscerri.ytdlnis.database.DBManager.SORTING
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
|
@ -41,6 +52,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
|
|||
private lateinit var templatesList: List<CommandTemplate>
|
||||
private lateinit var noResults: RelativeLayout
|
||||
private lateinit var mainActivity: MainActivity
|
||||
private lateinit var sortChip: Chip
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
@ -58,6 +70,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
|
|||
topAppBar = view.findViewById(R.id.logs_toolbar)
|
||||
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
|
||||
noResults = view.findViewById(R.id.no_results)
|
||||
sortChip = view.findViewById(R.id.sortChip)
|
||||
|
||||
templatesAdapter =
|
||||
TemplatesAdapter(
|
||||
|
|
@ -75,35 +88,90 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
|
|||
|
||||
|
||||
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
|
||||
commandTemplateViewModel.items.observe(viewLifecycleOwner) {
|
||||
commandTemplateViewModel.allItems.observe(viewLifecycleOwner) {
|
||||
if (it.isEmpty()) noResults.visibility = View.VISIBLE
|
||||
else noResults.visibility = View.GONE
|
||||
templatesList = it
|
||||
templatesAdapter.submitList(it)
|
||||
}
|
||||
|
||||
commandTemplateViewModel.getFilteredList().observe(viewLifecycleOwner) {
|
||||
templatesAdapter.submitList(it)
|
||||
templatesList = it
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
commandTemplateViewModel.sortOrder.observe(viewLifecycleOwner){
|
||||
if (it != null){
|
||||
when(it){
|
||||
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
|
||||
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandTemplateViewModel.sortType.observe(viewLifecycleOwner){
|
||||
if(it != null){
|
||||
when(it){
|
||||
CommandTemplateSortType.DATE -> sortChip.text = getString(R.string.date_added)
|
||||
CommandTemplateSortType.TITLE -> sortChip.text = getString(R.string.title)
|
||||
CommandTemplateSortType.LENGTH -> sortChip.text = getString(R.string.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initMenu()
|
||||
initChips()
|
||||
}
|
||||
|
||||
private fun initMenu() {
|
||||
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
|
||||
val itemId = m.itemId
|
||||
if (itemId == R.id.export_clipboard) {
|
||||
lifecycleScope.launch{
|
||||
withContext(Dispatchers.IO){
|
||||
commandTemplateViewModel.exportToClipboard()
|
||||
}
|
||||
Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
|
||||
val onActionExpandListener: MenuItem.OnActionExpandListener =
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
}else if (itemId == R.id.import_clipboard){
|
||||
lifecycleScope.launch{
|
||||
withContext(Dispatchers.IO){
|
||||
val count = commandTemplateViewModel.importFromClipboard()
|
||||
mainActivity.runOnUiThread{
|
||||
Snackbar.make(recyclerView, "${getString(R.string.items_imported)} (${count})", Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
topAppBar.menu.findItem(R.id.search_command)
|
||||
.setOnActionExpandListener(onActionExpandListener)
|
||||
val searchView = topAppBar.menu.findItem(R.id.search_command).actionView as SearchView?
|
||||
searchView!!.inputType = InputType.TYPE_CLASS_TEXT
|
||||
searchView.queryHint = getString(R.string.search_command_hint)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
topAppBar.menu.findItem(R.id.search_command).collapseActionView()
|
||||
commandTemplateViewModel.setQueryFilter(query)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
commandTemplateViewModel.setQueryFilter(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
topAppBar.setOnClickListener { scrollToTop() }
|
||||
|
||||
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
|
||||
when(m.itemId){
|
||||
R.id.export_clipboard -> {
|
||||
lifecycleScope.launch{
|
||||
withContext(Dispatchers.IO){
|
||||
commandTemplateViewModel.exportToClipboard()
|
||||
}
|
||||
Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
R.id.import_clipboard -> {
|
||||
lifecycleScope.launch{
|
||||
withContext(Dispatchers.IO){
|
||||
val count = commandTemplateViewModel.importFromClipboard()
|
||||
mainActivity.runOnUiThread{
|
||||
Snackbar.make(recyclerView, "${getString(R.string.items_imported)} (${count})", Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
|
|
@ -111,6 +179,45 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
|
|||
}
|
||||
|
||||
private fun initChips() {
|
||||
val sorting = view?.findViewById<Chip>(R.id.sortChip)
|
||||
sorting?.setOnClickListener {
|
||||
val sortSheet = BottomSheetDialog(requireContext())
|
||||
sortSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
sortSheet.setContentView(R.layout.command_templates_sort_sheet)
|
||||
|
||||
val date = sortSheet.findViewById<TextView>(R.id.date)
|
||||
val title = sortSheet.findViewById<TextView>(R.id.title)
|
||||
val length = sortSheet.findViewById<TextView>(R.id.length)
|
||||
|
||||
val sortOptions = listOf(date!!, title!!, length!!)
|
||||
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
when(commandTemplateViewModel.sortType.value!!) {
|
||||
CommandTemplateSortType.DATE -> changeSortIcon(date, commandTemplateViewModel.sortOrder.value!!)
|
||||
CommandTemplateSortType.TITLE -> changeSortIcon(title, commandTemplateViewModel.sortOrder.value!!)
|
||||
CommandTemplateSortType.LENGTH -> changeSortIcon(length, commandTemplateViewModel.sortOrder.value!!)
|
||||
}
|
||||
|
||||
date.setOnClickListener {
|
||||
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
commandTemplateViewModel.setSorting(CommandTemplateSortType.DATE)
|
||||
changeSortIcon(date, commandTemplateViewModel.sortOrder.value!!)
|
||||
}
|
||||
title.setOnClickListener {
|
||||
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
commandTemplateViewModel.setSorting(CommandTemplateSortType.TITLE)
|
||||
changeSortIcon(title, commandTemplateViewModel.sortOrder.value!!)
|
||||
}
|
||||
length.setOnClickListener {
|
||||
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
commandTemplateViewModel.setSorting(CommandTemplateSortType.LENGTH)
|
||||
changeSortIcon(length, commandTemplateViewModel.sortOrder.value!!)
|
||||
}
|
||||
val displayMetrics = DisplayMetrics()
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
sortSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
sortSheet.show()
|
||||
}
|
||||
|
||||
val new = view?.findViewById<Chip>(R.id.newTemplate)
|
||||
new?.setOnClickListener {
|
||||
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null,mainActivity, this, commandTemplateViewModel) {}
|
||||
|
|
@ -121,8 +228,25 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DownloadLogActivity"
|
||||
private fun changeSortIcon(item: TextView, order: SORTING){
|
||||
when(order){
|
||||
SORTING.DESC ->{
|
||||
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_up, 0,0,0)
|
||||
}
|
||||
SORTING.ASC -> {
|
||||
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_down, 0,0,0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
recyclerView.scrollToPosition(0)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
(topAppBar.parent as AppBarLayout).setExpanded(
|
||||
true,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(commandTemplate: CommandTemplate, index: Int) {
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@ class MoreFragment : Fragment() {
|
|||
terminateApp = view.findViewById(R.id.terminate)
|
||||
settings = view.findViewById(R.id.settings)
|
||||
|
||||
val navHostFragment = parentFragmentManager.findFragmentById(R.id.frame_layout)
|
||||
|
||||
if (mainSharedPreferences.getBoolean("log_downloads", false)) {
|
||||
logs.visibility = View.VISIBLE
|
||||
}else {
|
||||
logs.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
terminal.setOnClickListener {
|
||||
val intent = Intent(context, TerminalActivity::class.java)
|
||||
startActivity(intent)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ class WebViewActivity : BaseActivity() {
|
|||
generateBtn = toolbar.findViewById(R.id.generate)
|
||||
webViewCompose = findViewById(R.id.webview_compose)
|
||||
cookieManager = CookieManager.getInstance()
|
||||
cookieManager.removeAllCookies(null)
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
webViewClient = object : AccompanistWebViewClient() {
|
||||
|
|
@ -94,7 +93,6 @@ class WebViewActivity : BaseActivity() {
|
|||
Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
cookieManager.removeAllCookies(null)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.work.WorkInfo
|
|||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.deniscerri.ytdlnis.work.MoveCacheFilesWorker
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.io.File
|
||||
|
|
@ -30,8 +31,8 @@ class FolderSettingsFragment : BaseSettingsFragment() {
|
|||
private var commandPath: Preference? = null
|
||||
private var accessAllFiles : Preference? = null
|
||||
private var cacheDownloads : Preference? = null
|
||||
private var audioFilenameTemplate : EditTextPreference? = null
|
||||
private var videoFilenameTemplate : EditTextPreference? = null
|
||||
private var audioFilenameTemplate : Preference? = null
|
||||
private var videoFilenameTemplate : Preference? = null
|
||||
private var clearCache: Preference? = null
|
||||
private var moveCache: Preference? = null
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
|
|||
accessAllFiles!!.isVisible = false
|
||||
cacheDownloads!!.isEnabled = true
|
||||
}else{
|
||||
editor.putBoolean("cache_downloads", false).apply()
|
||||
editor.putBoolean("cache_downloads", true).apply()
|
||||
cacheDownloads!!.isEnabled = false
|
||||
}
|
||||
|
||||
|
|
@ -109,15 +110,30 @@ class FolderSettingsFragment : BaseSettingsFragment() {
|
|||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
val uri = Uri.parse("package:" + requireContext().packageName)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
accessAllFilesLauncher.launch(intent)
|
||||
true
|
||||
}
|
||||
|
||||
videoFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
|
||||
videoFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
|
||||
|
||||
videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader)s - %(title)s")
|
||||
audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
|
||||
audioFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
|
||||
audioFilenameTemplate?.summary = preferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
|
||||
|
||||
videoFilenameTemplate?.setOnPreferenceClickListener {
|
||||
UiUtil.showFilenameTemplateDialog(requireActivity(), videoFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.video)}]") {
|
||||
editor.putString("file_name_template", it).apply()
|
||||
videoFilenameTemplate?.summary = it
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
audioFilenameTemplate?.setOnPreferenceClickListener {
|
||||
UiUtil.showFilenameTemplateDialog(requireActivity(), audioFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]") {
|
||||
editor.putString("file_name_template_audio", it).apply()
|
||||
audioFilenameTemplate?.summary = it
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
var cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
|
||||
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
|
||||
|
|
@ -207,6 +223,15 @@ class FolderSettingsFragment : BaseSettingsFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private var accessAllFilesLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
accessAllFiles?.isVisible = false
|
||||
cacheDownloads?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun changePath(p: Preference?, data: Intent?, requestCode: Int) {
|
||||
val path = data!!.data.toString()
|
||||
p!!.summary = FileUtil.formatPath(data.data.toString())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.deniscerri.ytdlnis.ui.more.settings
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -8,6 +9,7 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
|
|
@ -32,6 +34,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
|||
private var highContrast: SwitchPreferenceCompat? = null
|
||||
private var locale: ListPreference? = null
|
||||
private var showTerminalShareIcon: SwitchPreferenceCompat? = null
|
||||
private var ignoreBatteryOptimization: Preference? = null
|
||||
|
||||
private var updateUtil: UpdateUtil? = null
|
||||
private var activeDownloadCount = 0
|
||||
|
|
@ -127,7 +130,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
|||
true
|
||||
}
|
||||
|
||||
var ignoreBatteryOptimization = findPreference<Preference>("ignore_battery")
|
||||
ignoreBatteryOptimization = findPreference<Preference>("ignore_battery")
|
||||
val packageName: String = requireContext().packageName
|
||||
val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
|
|
@ -140,8 +143,16 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
|||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
intent.data = Uri.parse("package:" + requireContext().packageName)
|
||||
startActivity(intent)
|
||||
ignoreBatteryLauncher.launch(intent)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private var ignoreBatteryLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
ignoreBatteryOptimization?.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.LayoutDirection
|
||||
import android.util.Log
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
|
|
@ -171,7 +172,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
|||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS), "YTDLnis_${BuildConfig.VERSION_NAME}_${currentTime.get(
|
||||
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
|
||||
Calendar.DAY_OF_MONTH)}.json")
|
||||
Calendar.DAY_OF_MONTH)} [${currentTime.get(Calendar.MILLISECOND)}.json")
|
||||
saveFile.delete()
|
||||
saveFile.createNewFile()
|
||||
saveFile.writeText(Gson().toJson(json))
|
||||
|
|
@ -223,15 +224,14 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
|||
private fun backupSettings(preferences: SharedPreferences) : JsonArray {
|
||||
runCatching {
|
||||
val prefs = preferences.all
|
||||
prefs.remove("app_language")
|
||||
val arr = JsonArray()
|
||||
prefs.forEach {
|
||||
if (it.key != "app_language"){
|
||||
val obj = JsonObject()
|
||||
obj.addProperty("key", it.key)
|
||||
obj.addProperty("value", it.value.toString())
|
||||
obj.addProperty("type", it.value!!::class.simpleName)
|
||||
arr.add(obj)
|
||||
}
|
||||
val obj = JsonObject()
|
||||
obj.addProperty("key", it.key)
|
||||
obj.addProperty("value", it.value.toString())
|
||||
obj.addProperty("type", it.value!!::class.simpleName)
|
||||
arr.add(obj)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
|
@ -398,27 +398,26 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
|||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
|
||||
clear()
|
||||
val prefs = json.getAsJsonArray("settings")
|
||||
val preferencesKeys = preferences.all.map { it.key }
|
||||
prefs.forEach {
|
||||
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
|
||||
if (preferencesKeys.contains(key)){
|
||||
when(it.asJsonObject.get("type").toString().replace("\"", "")){
|
||||
"String" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("\"", "")
|
||||
putString(key, value)
|
||||
}
|
||||
"Boolean" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
|
||||
putBoolean(key, value)
|
||||
}
|
||||
"Int" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
|
||||
putInt(key, value)
|
||||
}
|
||||
"HashSet" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
|
||||
putStringSet(key, value.toHashSet())
|
||||
}
|
||||
when(it.asJsonObject.get("type").toString().replace("\"", "")){
|
||||
"String" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("\"", "")
|
||||
putString(key, value)
|
||||
}
|
||||
"Boolean" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
|
||||
Log.e("REST", value.toString())
|
||||
Log.e("REST", key)
|
||||
putBoolean(key, value)
|
||||
}
|
||||
"Int" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
|
||||
putInt(key, value)
|
||||
}
|
||||
"HashSet" -> {
|
||||
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
|
||||
putStringSet(key, value.toHashSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.deniscerri.ytdlnis.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.os.Looper
|
||||
|
|
@ -16,7 +15,6 @@ 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.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
|
|
@ -387,17 +385,13 @@ class InfoUtil(private val context: Context) {
|
|||
val items = arrayListOf<ResultItem?>()
|
||||
val url = "$pipedURL/trending?region=$countryCODE"
|
||||
val res = genericArrayRequest(url)
|
||||
try {
|
||||
for (i in 0 until res.length()) {
|
||||
val element = res.getJSONObject(i)
|
||||
if (element.getInt("duration") < 0) continue
|
||||
element.put("uploader", element.getString("uploaderName"))
|
||||
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
|
||||
if (v == null || v.thumb.isEmpty()) continue
|
||||
items.add(v)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
for (i in 0 until res.length()) {
|
||||
val element = res.getJSONObject(i)
|
||||
if (element.getInt("duration") < 0) continue
|
||||
element.put("uploader", element.getString("uploaderName"))
|
||||
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
|
||||
if (v == null || v.thumb.isEmpty()) continue
|
||||
items.add(v)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
|
@ -440,51 +434,43 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
|
||||
private fun getFormatsFromYTDL(url: String) : List<Format> {
|
||||
try {
|
||||
val request = YoutubeDLRequest(url)
|
||||
request.addOption("--print", "%(formats)s")
|
||||
request.addOption("--print", "%(duration)s")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
val request = YoutubeDLRequest(url)
|
||||
request.addOption("--print", "%(formats)s")
|
||||
request.addOption("--print", "%(duration)s")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()) {
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
|
||||
|
||||
val res = YoutubeDL.getInstance().execute(request)
|
||||
val results: Array<String?> = try {
|
||||
val lineSeparator = System.getProperty("line.separator")
|
||||
res.out.split(lineSeparator!!).toTypedArray()
|
||||
} catch (e: Exception) {
|
||||
arrayOf(res.out)
|
||||
}
|
||||
val json = results[0]
|
||||
val jsonArray = JSONArray(json)
|
||||
|
||||
return parseYTDLFormats(jsonArray)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Looper.prepare().run {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()) {
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
|
||||
|
||||
val res = YoutubeDL.getInstance().execute(request)
|
||||
val results: Array<String?> = try {
|
||||
val lineSeparator = System.getProperty("line.separator")
|
||||
res.out.split(lineSeparator!!).toTypedArray()
|
||||
} catch (e: Exception) {
|
||||
arrayOf(res.out)
|
||||
}
|
||||
val json = results[0]
|
||||
val jsonArray = JSONArray(json)
|
||||
|
||||
return parseYTDLFormats(jsonArray)
|
||||
}
|
||||
|
||||
fun getFormatsMultiple(urls: List<String>, progress: (progress: List<Format>) -> Unit){
|
||||
|
|
@ -561,147 +547,139 @@ class InfoUtil(private val context: Context) {
|
|||
fun getFromYTDL(query: String): ArrayList<ResultItem?> {
|
||||
val items = arrayListOf<ResultItem?>()
|
||||
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
|
||||
try {
|
||||
val request : YoutubeDLRequest
|
||||
if (query.contains("http")){
|
||||
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 = 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("${searchEngine}25:\"${query}\"")
|
||||
}
|
||||
}
|
||||
|
||||
request.addOption("--flat-playlist")
|
||||
request.addOption("-j")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
request.addOption("--flat-playlist")
|
||||
request.addOption("-j")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
}
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
|
||||
|
||||
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||
val results: List<String?> = try {
|
||||
val lineSeparator = System.getProperty("line.separator")
|
||||
youtubeDLResponse.out.split(lineSeparator!!)
|
||||
} catch (e: Exception) {
|
||||
listOf(youtubeDLResponse.out)
|
||||
}.filter { it.isNotBlank() }
|
||||
for (result in results) {
|
||||
if (result.isNullOrBlank()) continue
|
||||
val jsonObject = JSONObject(result)
|
||||
val title = if (jsonObject.has("title")) {
|
||||
if (jsonObject.getString("title") == "[Private video]") continue
|
||||
jsonObject.getString("title")
|
||||
} else {
|
||||
jsonObject.getString("webpage_url_basename")
|
||||
}
|
||||
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
|
||||
}
|
||||
}
|
||||
var duration = ""
|
||||
runCatching {
|
||||
if (jsonObject.has("duration")) {
|
||||
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
var thumb: String? = ""
|
||||
if (jsonObject.has("thumbnail")) {
|
||||
thumb = jsonObject.getString("thumbnail")
|
||||
} else if (jsonObject.has("thumbnails")) {
|
||||
val thumbs = jsonObject.getJSONArray("thumbnails")
|
||||
if (thumbs.length() > 0){
|
||||
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
|
||||
}
|
||||
}
|
||||
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
|
||||
var playlistTitle: String? = ""
|
||||
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
|
||||
if(playlistTitle.equals(query)) playlistTitle = ""
|
||||
|
||||
if (playlistTitle?.isNotBlank() == true){
|
||||
playlistTitle += "[${jsonObject.getString("playlist_index")}]"
|
||||
}
|
||||
|
||||
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
|
||||
val formats : ArrayList<Format> = parseYTDLFormats(formatsInJSON)
|
||||
|
||||
val chaptersInJSON = if (jsonObject.has("chapters") && jsonObject.get("chapters") is JSONArray) jsonObject.getJSONArray("chapters") else null
|
||||
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
|
||||
var chapters : ArrayList<ChapterItem> = arrayListOf()
|
||||
|
||||
if (chaptersInJSON != null){
|
||||
chapters = Gson().fromJson(chaptersInJSON.toString(), listType)
|
||||
}
|
||||
|
||||
var urls = "";
|
||||
if(jsonObject.has("requested_formats")) {
|
||||
val requestedFormats = jsonObject.getJSONArray("requested_formats")
|
||||
val urlList = mutableListOf<String>()
|
||||
val length = requestedFormats.length()-1
|
||||
for (i in length downTo 0) {
|
||||
urlList.add(requestedFormats.getJSONObject(i).getString("url"))
|
||||
}
|
||||
|
||||
urls = urlList.joinToString("\n")
|
||||
}
|
||||
|
||||
val url = if (jsonObject.has("url") && results.size > 1){
|
||||
jsonObject.getString("url")
|
||||
}else{
|
||||
if (Patterns.WEB_URL.matcher(query).matches()){
|
||||
query
|
||||
}else{
|
||||
jsonObject.getString("webpage_url")
|
||||
}
|
||||
}
|
||||
|
||||
items.add(ResultItem(0,
|
||||
url,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
thumb!!,
|
||||
website,
|
||||
playlistTitle!!,
|
||||
formats,
|
||||
urls,
|
||||
chapters
|
||||
)
|
||||
)
|
||||
}
|
||||
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||
val results: List<String?> = try {
|
||||
val lineSeparator = System.getProperty("line.separator")
|
||||
youtubeDLResponse.out.split(lineSeparator!!)
|
||||
} catch (e: Exception) {
|
||||
val intent = Intent(context, ErrorDialogActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.putExtra("title", context.resources.getString(R.string.no_results))
|
||||
intent.putExtra("message", e.message)
|
||||
context.startActivity(intent)
|
||||
e.printStackTrace()
|
||||
listOf(youtubeDLResponse.out)
|
||||
}.filter { it.isNotBlank() }
|
||||
for (result in results) {
|
||||
if (result.isNullOrBlank()) continue
|
||||
val jsonObject = JSONObject(result)
|
||||
val title = if (jsonObject.has("title")) {
|
||||
if (jsonObject.getString("title") == "[Private video]") continue
|
||||
jsonObject.getString("title")
|
||||
} else {
|
||||
jsonObject.getString("webpage_url_basename")
|
||||
}
|
||||
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
|
||||
}
|
||||
}
|
||||
var duration = ""
|
||||
runCatching {
|
||||
if (jsonObject.has("duration")) {
|
||||
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
var thumb: String? = ""
|
||||
if (jsonObject.has("thumbnail")) {
|
||||
thumb = jsonObject.getString("thumbnail")
|
||||
} else if (jsonObject.has("thumbnails")) {
|
||||
val thumbs = jsonObject.getJSONArray("thumbnails")
|
||||
if (thumbs.length() > 0){
|
||||
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
|
||||
}
|
||||
}
|
||||
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
|
||||
var playlistTitle: String? = ""
|
||||
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
|
||||
if(playlistTitle.equals(query)) playlistTitle = ""
|
||||
|
||||
if (playlistTitle?.isNotBlank() == true){
|
||||
playlistTitle += "[${jsonObject.getString("playlist_index")}]"
|
||||
}
|
||||
|
||||
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
|
||||
val formats : ArrayList<Format> = parseYTDLFormats(formatsInJSON)
|
||||
|
||||
val chaptersInJSON = if (jsonObject.has("chapters") && jsonObject.get("chapters") is JSONArray) jsonObject.getJSONArray("chapters") else null
|
||||
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
|
||||
var chapters : ArrayList<ChapterItem> = arrayListOf()
|
||||
|
||||
if (chaptersInJSON != null){
|
||||
chapters = Gson().fromJson(chaptersInJSON.toString(), listType)
|
||||
}
|
||||
|
||||
var urls = "";
|
||||
if(jsonObject.has("requested_formats")) {
|
||||
val requestedFormats = jsonObject.getJSONArray("requested_formats")
|
||||
val urlList = mutableListOf<String>()
|
||||
val length = requestedFormats.length()-1
|
||||
for (i in length downTo 0) {
|
||||
urlList.add(requestedFormats.getJSONObject(i).getString("url"))
|
||||
}
|
||||
|
||||
urls = urlList.joinToString("\n")
|
||||
}
|
||||
|
||||
val url = if (jsonObject.has("url") && results.size > 1){
|
||||
jsonObject.getString("url")
|
||||
}else{
|
||||
if (Patterns.WEB_URL.matcher(query).matches()){
|
||||
query
|
||||
}else{
|
||||
jsonObject.getString("webpage_url")
|
||||
}
|
||||
}
|
||||
|
||||
items.add(ResultItem(0,
|
||||
url,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
thumb!!,
|
||||
website,
|
||||
playlistTitle!!,
|
||||
formats,
|
||||
urls,
|
||||
chapters
|
||||
)
|
||||
)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
|
@ -748,89 +726,81 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
|
||||
fun getMissingInfo(url: String): ResultItem? {
|
||||
try {
|
||||
val request = YoutubeDLRequest(url)
|
||||
request.addOption("--flat-playlist")
|
||||
request.addOption("-J")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
val request = YoutubeDLRequest(url)
|
||||
request.addOption("--flat-playlist")
|
||||
request.addOption("-J")
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
request.addOption("--socket-timeout", "5")
|
||||
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||
val jsonObject = JSONObject(youtubeDLResponse.out)
|
||||
|
||||
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
|
||||
}
|
||||
}
|
||||
|
||||
var duration = ""
|
||||
runCatching {
|
||||
if (jsonObject.has("duration")) {
|
||||
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
var thumb: String? = ""
|
||||
if (jsonObject.has("thumbnail")) {
|
||||
thumb = jsonObject.getString("thumbnail")
|
||||
} else if (jsonObject.has("thumbnails")) {
|
||||
val thumbs = jsonObject.getJSONArray("thumbnails")
|
||||
if (thumbs.length() > 0){
|
||||
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
|
||||
}
|
||||
}
|
||||
|
||||
val isPlaylist = jsonObject.has("playlist_count")
|
||||
return ResultItem(
|
||||
0,
|
||||
url,
|
||||
if (isPlaylist){
|
||||
"[${jsonObject.getInt("playlist_count")} Items] ${jsonObject.getString("title")}"
|
||||
}else{
|
||||
jsonObject.getString("title")
|
||||
},
|
||||
author,
|
||||
duration,
|
||||
thumb!!,
|
||||
jsonObject.getString("extractor"),
|
||||
if (isPlaylist) jsonObject.getString("title") else "",
|
||||
arrayListOf(),
|
||||
"",
|
||||
arrayListOf(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Looper.prepare().run {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
|
||||
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
|
||||
|
||||
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||
val jsonObject = JSONObject(youtubeDLResponse.out)
|
||||
|
||||
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
|
||||
if (author.isEmpty() || author == "null"){
|
||||
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
|
||||
}
|
||||
}
|
||||
|
||||
var duration = ""
|
||||
runCatching {
|
||||
if (jsonObject.has("duration")) {
|
||||
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
var thumb: String? = ""
|
||||
if (jsonObject.has("thumbnail")) {
|
||||
thumb = jsonObject.getString("thumbnail")
|
||||
} else if (jsonObject.has("thumbnails")) {
|
||||
val thumbs = jsonObject.getJSONArray("thumbnails")
|
||||
if (thumbs.length() > 0){
|
||||
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
|
||||
}
|
||||
}
|
||||
|
||||
val isPlaylist = jsonObject.has("playlist_count")
|
||||
return ResultItem(
|
||||
0,
|
||||
url,
|
||||
if (isPlaylist){
|
||||
""
|
||||
}else{
|
||||
jsonObject.getString("title")
|
||||
},
|
||||
author,
|
||||
duration,
|
||||
thumb!!,
|
||||
jsonObject.getString("extractor"),
|
||||
if (isPlaylist) jsonObject.getString("title") else "",
|
||||
arrayListOf(),
|
||||
"",
|
||||
arrayListOf(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -983,9 +953,9 @@ class InfoUtil(private val context: Context) {
|
|||
val downDir : File
|
||||
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
|
||||
downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
|
||||
request.addOption("--no-quiet")
|
||||
request.addOption("-no-simulate")
|
||||
request.addOption("--print", "after_video:'%(filename)s'")
|
||||
request.addOption("--progress")
|
||||
request.addOption("-v")
|
||||
}else{
|
||||
val cacheDir = FileUtil.getCachePath(context)
|
||||
downDir = File(cacheDir, downloadItem.id.toString())
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ package com.deniscerri.ytdlnis.util
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.graphics.drawable.shapes.OvalShape
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.text.format.DateFormat
|
||||
|
|
@ -34,22 +36,32 @@ import androidx.appcompat.content.res.AppCompatResources
|
|||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.allViews
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.App
|
||||
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
|
||||
import com.deniscerri.ytdlnis.MainActivity
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.Format
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
||||
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
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
|
|
@ -61,6 +73,7 @@ import com.google.android.material.datepicker.CalendarConstraints
|
|||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
|
|
@ -68,7 +81,9 @@ import com.google.android.material.timepicker.TimeFormat
|
|||
import com.neo.highlight.core.Highlight
|
||||
import com.neo.highlight.util.listener.HighlightTextWatcher
|
||||
import com.neo.highlight.util.scheme.ColorScheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
|
@ -416,17 +431,17 @@ object UiUtil {
|
|||
author!!.text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" }
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
val btn = bottomSheet.findViewById<FloatingActionButton>(R.id.download_button_type)
|
||||
|
||||
when (item.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_music)
|
||||
btn?.setImageResource(R.drawable.ic_music)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_video)
|
||||
btn?.setImageResource(R.drawable.ic_video)
|
||||
}
|
||||
else -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_terminal)
|
||||
btn?.setImageResource(R.drawable.ic_terminal)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -542,6 +557,135 @@ object UiUtil {
|
|||
)
|
||||
}
|
||||
|
||||
fun showHistoryItemDetailsCard(
|
||||
item: HistoryItem?,
|
||||
context: Activity,
|
||||
isPresent: Boolean,
|
||||
redownloadItem: (HistoryItem) -> Unit,
|
||||
redownloadShowDownloadCard: (HistoryItem) -> Unit,
|
||||
){
|
||||
val bottomSheet = BottomSheetDialog(context)
|
||||
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
|
||||
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<FloatingActionButton>(R.id.download_button_type)
|
||||
|
||||
if (item.type == DownloadViewModel.Type.audio) {
|
||||
if (isPresent) {
|
||||
btn?.setImageResource(R.drawable.ic_music_downloaded)
|
||||
} else {
|
||||
btn?.setImageResource(R.drawable.ic_music)
|
||||
}
|
||||
} else if (item.type == DownloadViewModel.Type.video) {
|
||||
if (isPresent) {
|
||||
btn?.setImageResource(R.drawable.ic_video_downloaded)
|
||||
} else {
|
||||
btn?.setImageResource(R.drawable.ic_video)
|
||||
}
|
||||
}else{
|
||||
btn?.setImageResource(R.drawable.ic_terminal)
|
||||
}
|
||||
|
||||
if (isPresent){
|
||||
btn?.setOnClickListener {
|
||||
shareFileIntent(context, listOf(item.downloadPath))
|
||||
}
|
||||
}
|
||||
|
||||
val time = bottomSheet.findViewById<TextView>(R.id.time)
|
||||
val formatNote = bottomSheet.findViewById<TextView>(R.id.format_note)
|
||||
val container = bottomSheet.findViewById<TextView>(R.id.container_chip)
|
||||
val codec = bottomSheet.findViewById<TextView>(R.id.codec)
|
||||
val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size)
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = item.time * 1000L
|
||||
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
|
||||
time.isClickable = false
|
||||
|
||||
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 file = File(item.downloadPath)
|
||||
val fileSizeReadable = FileUtil.convertFileSize(if (file.exists()) file.length() else 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 = item.id
|
||||
link.setOnClickListener{
|
||||
openLinkIntent(context, item.url, bottomSheet)
|
||||
}
|
||||
link.setOnLongClickListener{
|
||||
copyLinkToClipBoard(context, item.url, bottomSheet)
|
||||
true
|
||||
}
|
||||
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
|
||||
remove!!.tag = item.id
|
||||
remove.setOnClickListener{
|
||||
showRemoveHistoryItemDialog(item, context, delete = { item, deleteFile ->
|
||||
|
||||
})
|
||||
bottomSheet.dismiss()
|
||||
}
|
||||
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
|
||||
openFile!!.tag = item.id
|
||||
openFile.setOnClickListener{
|
||||
openFileIntent(context, item.downloadPath)
|
||||
}
|
||||
|
||||
val redownload = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
|
||||
redownload!!.tag = item.id
|
||||
redownload.setOnClickListener{
|
||||
redownloadItem(item)
|
||||
bottomSheet.cancel()
|
||||
}
|
||||
|
||||
redownload.setOnLongClickListener {
|
||||
redownloadShowDownloadCard(item)
|
||||
bottomSheet.cancel()
|
||||
true
|
||||
}
|
||||
|
||||
if (!isPresent) openFile.visibility = View.GONE
|
||||
else redownload.visibility = View.GONE
|
||||
|
||||
bottomSheet.show()
|
||||
val displayMetrics = DisplayMetrics()
|
||||
context.windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
bottomSheet.window!!.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
fun showFormatDetails(format: Format, activity: Activity){
|
||||
val bottomSheet = BottomSheetDialog(activity)
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
|
@ -653,16 +797,16 @@ object UiUtil {
|
|||
builder.setView(inputLayout)
|
||||
builder.setPositiveButton(
|
||||
context.getString(R.string.ok)
|
||||
) { dialog: DialogInterface?, which: Int ->
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
ok(editText.text.toString())
|
||||
}
|
||||
|
||||
// handle the negative button of the alert dialog
|
||||
builder.setNegativeButton(
|
||||
context.getString(R.string.cancel)
|
||||
) { dialog: DialogInterface?, which: Int -> }
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
|
||||
builder.setNeutralButton("?") { dialog: DialogInterface?, which: Int ->
|
||||
builder.setNeutralButton("?") { _: DialogInterface?, _: Int ->
|
||||
val browserIntent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/yt-dlp/yt-dlp#subtitle-options"))
|
||||
context.startActivity(browserIntent)
|
||||
|
|
@ -670,7 +814,7 @@ object UiUtil {
|
|||
|
||||
|
||||
val dialog = builder.create()
|
||||
editText.doOnTextChanged { text, start, before, count ->
|
||||
editText.doOnTextChanged { _, _, _, _ ->
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
|
||||
}
|
||||
dialog.show()
|
||||
|
|
@ -923,35 +1067,14 @@ object UiUtil {
|
|||
|
||||
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
|
||||
filenameTemplate.setOnClickListener {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(context.getString(R.string.file_name_template))
|
||||
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
|
||||
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
|
||||
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
|
||||
if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
|
||||
editText.setText(items[0].customFileNameTemplate)
|
||||
val currentFilename = if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
|
||||
items[0].customFileNameTemplate
|
||||
}else {
|
||||
""
|
||||
}
|
||||
editText.setSelection(editText.text.length)
|
||||
builder.setView(inputLayout)
|
||||
builder.setPositiveButton(
|
||||
context.getString(R.string.ok)
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
filenameTemplateSet(editText.text.toString())
|
||||
showFilenameTemplateDialog(context, currentFilename) {
|
||||
filenameTemplateSet(it)
|
||||
}
|
||||
|
||||
// handle the negative button of the alert dialog
|
||||
builder.setNegativeButton(
|
||||
context.getString(R.string.cancel)
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
editText!!.postDelayed({
|
||||
editText.requestFocus()
|
||||
imm.showSoftInput(editText, 0)
|
||||
}, 300)
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1022,35 +1145,14 @@ object UiUtil {
|
|||
|
||||
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
|
||||
filenameTemplate.setOnClickListener {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(context.getString(R.string.file_name_template))
|
||||
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
|
||||
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
|
||||
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
|
||||
if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
|
||||
editText.setText(items[0].customFileNameTemplate)
|
||||
val currentFilename = if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
|
||||
items[0].customFileNameTemplate
|
||||
}else {
|
||||
""
|
||||
}
|
||||
editText.setSelection(editText.text.length)
|
||||
builder.setView(inputLayout)
|
||||
builder.setPositiveButton(
|
||||
context.getString(R.string.ok)
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
filenameTemplateSet(editText.text.toString())
|
||||
showFilenameTemplateDialog(context, currentFilename) {
|
||||
filenameTemplateSet(it)
|
||||
}
|
||||
|
||||
// handle the negative button of the alert dialog
|
||||
builder.setNegativeButton(
|
||||
context.getString(R.string.cancel)
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
editText!!.postDelayed({
|
||||
editText.requestFocus()
|
||||
imm.showSoftInput(editText, 0)
|
||||
}, 300)
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
|
||||
}
|
||||
|
||||
val sponsorBlock = view.findViewById<Chip>(R.id.sponsorblock_filters)
|
||||
|
|
@ -1222,9 +1324,6 @@ object UiUtil {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fun EditText.setTextAndRecalculateWidth(t : String){
|
||||
val scale = context.resources.displayMetrics.density
|
||||
this.setText(t)
|
||||
|
|
@ -1239,4 +1338,258 @@ object UiUtil {
|
|||
}
|
||||
this.requestLayout()
|
||||
}
|
||||
|
||||
fun handleResultResponse(context: Activity, it: ResultViewModel.ResultsUiState){
|
||||
val title = context.getString(it.errorMessage!!.first)
|
||||
val message = it.errorMessage!!.second
|
||||
|
||||
val errDialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
|
||||
for (a in it.actions!!){
|
||||
when(a.second){
|
||||
ResultViewModel.ResultAction.COPY_LOG -> {
|
||||
errDialog.setPositiveButton(a.first) { d:DialogInterface?, _:Int ->
|
||||
val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setText(message)
|
||||
d?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
errDialog.show()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun handleDownloadsResponse(context: MainActivity, it: DownloadViewModel.DownloadsUiState, downloadViewModel: DownloadViewModel){
|
||||
val downloadAnywayAction = it.actions?.first { it.second == DownloadViewModel.DownloadsAction.DOWNLOAD_ANYWAY}
|
||||
if (downloadAnywayAction != null){
|
||||
if (downloadAnywayAction.third == null) return
|
||||
val downloads =downloadAnywayAction.third!!.toMutableList()
|
||||
val ids = downloadAnywayAction.third!!.map { it.id }
|
||||
val titles = downloadAnywayAction.third!!.map { it.title }
|
||||
|
||||
|
||||
val title = context.getString(it.errorMessage!!.first)
|
||||
val message = context.getString(it.errorMessage!!.second)
|
||||
|
||||
val errDialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
|
||||
var dialog: Dialog? = null
|
||||
|
||||
val linearLayout = context.layoutInflater.inflate(R.layout.already_exists_card, null) as LinearLayout
|
||||
ids.forEachIndexed { index, id ->
|
||||
val alreadyExistsItem = context.layoutInflater.inflate(R.layout.already_exists_item, null)
|
||||
alreadyExistsItem.tag = id.toString()
|
||||
val ttle = alreadyExistsItem.findViewById<Button>(R.id.already_exists_title)
|
||||
ttle.text = "${index + 1}. ${titles[index]}"
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val editBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_edit)
|
||||
var downloadItem: DownloadItem = downloadViewModel.getItemByID(id)
|
||||
val historyItem: HistoryItem? = downloadViewModel.getHistoryItemById(id)
|
||||
|
||||
if (historyItem != null){
|
||||
val idx = downloads.indexOfFirst { it.id == historyItem.id }
|
||||
downloadItem = downloadViewModel.createDownloadItemFromHistory(historyItem)
|
||||
downloadItem.id = historyItem.id
|
||||
downloads[idx] = downloadItem
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main){
|
||||
editBtn.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
editBtn.setOnClickListener {
|
||||
val resultItem = downloadViewModel.createResultItemFromDownload(downloadItem)
|
||||
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
|
||||
override fun onDownloadItemUpdate(
|
||||
resultItemID: Long,
|
||||
item: DownloadItem
|
||||
) {
|
||||
context.lifecycleScope.launch {
|
||||
val idx = downloads.indexOfFirst { it.id == item.id }
|
||||
downloads[idx] = item
|
||||
withContext(Dispatchers.Main){
|
||||
ttle.text = "${index + 1}. ${item.title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, onItemUpdated)
|
||||
bottomSheet.show(context.supportFragmentManager, "configureDownloadSingleSheet")
|
||||
}
|
||||
|
||||
if (historyItem != null){
|
||||
ttle.setOnClickListener {
|
||||
showHistoryItemDetailsCard(historyItem, context, isPresent = true,
|
||||
redownloadItem = { },
|
||||
redownloadShowDownloadCard = {
|
||||
val sheet = DownloadBottomSheetDialog(
|
||||
result = downloadViewModel.createResultItemFromHistory(it),
|
||||
type = it.type
|
||||
)
|
||||
sheet.show(context.supportFragmentManager, "downloadSingleSheet")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttle.setOnLongClickListener {
|
||||
showGenericDeleteDialog(context, ttle.text.toString(), accepted = {
|
||||
linearLayout.removeView(alreadyExistsItem)
|
||||
if (linearLayout.childCount == 0){
|
||||
dialog?.dismiss()
|
||||
}
|
||||
})
|
||||
true
|
||||
}
|
||||
|
||||
linearLayout.addView(alreadyExistsItem)
|
||||
}
|
||||
|
||||
errDialog.setView(linearLayout)
|
||||
|
||||
errDialog.setPositiveButton(downloadAnywayAction.first) { d:DialogInterface?, _:Int ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
linearLayout.allViews.forEach {view ->
|
||||
val downloadItem = downloads.first { it.id == (view.tag as String).toLong()}
|
||||
downloadItem.id = 0
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem), true)
|
||||
}
|
||||
}
|
||||
d?.dismiss()
|
||||
}
|
||||
|
||||
errDialog.setNegativeButton(R.string.schedule) { d:DialogInterface?, _:Int ->
|
||||
showDatePicker(context.supportFragmentManager) { calendar ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val items = mutableListOf<DownloadItem>()
|
||||
linearLayout.children.forEach {view ->
|
||||
val downloadItem = downloads.first { it.id == (view.tag as String).toLong()}
|
||||
downloadItem.downloadStartTime = calendar.timeInMillis
|
||||
downloadItem.id = 0
|
||||
items.add(downloadItem)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
val chunks = items.chunked(10)
|
||||
for (c in chunks) {
|
||||
downloadViewModel.queueDownloads(c, true)
|
||||
}
|
||||
val first = items.first()
|
||||
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime)
|
||||
withContext(Dispatchers.Main){
|
||||
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main){
|
||||
d?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog = errDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun showGenericDeleteDialog(context: Context, itemTitle: String, accepted: () -> Unit){
|
||||
val deleteDialog = MaterialAlertDialogBuilder(context)
|
||||
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + itemTitle + "\"!")
|
||||
deleteDialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
accepted()
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
fun showRemoveHistoryItemDialog(item: HistoryItem, context: Activity, delete: (item: HistoryItem, deleteFile: Boolean) -> Unit){
|
||||
val deleteFile = booleanArrayOf(false)
|
||||
val deleteDialog = MaterialAlertDialogBuilder(context)
|
||||
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
|
||||
val path = item.downloadPath
|
||||
val file = File(path)
|
||||
if (file.exists() && path.isNotEmpty()) {
|
||||
deleteDialog.setMultiChoiceItems(
|
||||
arrayOf(context.getString(R.string.delete_file_too)),
|
||||
booleanArrayOf(false)
|
||||
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
|
||||
}
|
||||
deleteDialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
delete(item, deleteFile[0])
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
fun showFilenameTemplateDialog(context: Activity, currentFilename: String, dialogTitle: String = context.getString(R.string.file_name_template), filenameSelected: (f: String) -> Unit){
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(dialogTitle)
|
||||
val view = context.layoutInflater.inflate(R.layout.filename_template_dialog, null)
|
||||
val editText = view.findViewById<EditText>(R.id.filename_edittext)
|
||||
view.findViewById<TextInputLayout>(R.id.filename).hint = context.getString(R.string.file_name_template)
|
||||
editText.setText(currentFilename)
|
||||
editText.setSelection(editText.text.length)
|
||||
builder.setView(view)
|
||||
builder.setPositiveButton(
|
||||
context.getString(R.string.ok)
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
filenameSelected(editText.text.toString())
|
||||
}
|
||||
|
||||
// handle the negative button of the alert dialog
|
||||
builder.setNegativeButton(
|
||||
context.getString(R.string.cancel)
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
|
||||
view.findViewById<View>(R.id.suggested).visibility = View.GONE
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
editText!!.postDelayed({
|
||||
editText.requestFocus()
|
||||
imm.showSoftInput(editText, 0)
|
||||
}, 300)
|
||||
|
||||
//handle suggestion chips
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup)
|
||||
val chips = mutableListOf<Chip>()
|
||||
context.getStringArray(R.array.filename_templates).forEachIndexed { index, s ->
|
||||
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
|
||||
tmp.text = s.split("___")[0]
|
||||
tmp.id = index
|
||||
if (Build.VERSION.SDK_INT >= 26){
|
||||
tmp.tooltipText = s.split("___")[1]
|
||||
}
|
||||
|
||||
tmp.setOnClickListener {
|
||||
val c = it as Chip
|
||||
if(!c.isChecked){
|
||||
editText.setText(editText.text.toString().replace(c.text.toString(), ""))
|
||||
editText.setSelection(editText.text.length)
|
||||
}else{
|
||||
editText.append(c.text)
|
||||
}
|
||||
}
|
||||
|
||||
chips.add(tmp)
|
||||
}
|
||||
withContext(Dispatchers.Main){
|
||||
view.findViewById<View>(R.id.suggested).visibility = View.VISIBLE
|
||||
chips.forEach {
|
||||
it.isChecked = editText.text.contains(it.text)
|
||||
chipGroup!!.addView(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ class UpdateUtil(var context: Context) {
|
|||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
val skippedVersions = sharedPreferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
|
||||
val res = getGithubReleases()
|
||||
|
||||
if (res.isEmpty()){
|
||||
|
|
@ -67,6 +68,8 @@ class UpdateUtil(var context: Context) {
|
|||
isInLatest = false
|
||||
}
|
||||
|
||||
if (skippedVersions.contains(v.tag_name)) isInLatest = true
|
||||
|
||||
if (isInLatest){
|
||||
result(context.getString(R.string.you_are_in_latest_version))
|
||||
return
|
||||
|
|
@ -77,6 +80,11 @@ class UpdateUtil(var context: Context) {
|
|||
.setTitle(v.tag_name)
|
||||
.setMessage(v.body)
|
||||
.setIcon(R.drawable.ic_update_app)
|
||||
.setNeutralButton(R.string.skip){ d: DialogInterface?, _:Int ->
|
||||
skippedVersions.add(v.tag_name)
|
||||
sharedPreferences.edit().putString("skip_updates", skippedVersions.joinToString(",")).apply()
|
||||
d?.dismiss()
|
||||
}
|
||||
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
|
||||
.setPositiveButton(context.resources.getString(R.string.update)) { _: DialogInterface?, _: Int ->
|
||||
runCatching {
|
||||
|
|
|
|||
|
|
@ -136,13 +136,10 @@ class DownloadWorker(
|
|||
)
|
||||
|
||||
|
||||
if (logDownloads){
|
||||
runBlocking {
|
||||
logItem.id = logRepo.insert(logItem)
|
||||
downloadItem.logID = logItem.id
|
||||
dao.update(downloadItem)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
logItem.id = logRepo.insert(logItem)
|
||||
downloadItem.logID = logItem.id
|
||||
dao.update(downloadItem)
|
||||
}
|
||||
|
||||
val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()
|
||||
|
|
@ -156,10 +153,8 @@ class DownloadWorker(
|
|||
line, progress.toInt(), 0, title,
|
||||
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
|
||||
)
|
||||
if (logDownloads){
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
logRepo.update(line, logItem.id)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
logRepo.update(line, logItem.id)
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
|
|
@ -169,7 +164,7 @@ class DownloadWorker(
|
|||
|
||||
if (noCache){
|
||||
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id))
|
||||
val p = it.out.split("\r")
|
||||
val p = it.out.split("\n")
|
||||
.filter { it.startsWith("'/storage") }
|
||||
.map { it.removePrefix("'") }
|
||||
.map { it.removeSuffix("'\n") }
|
||||
|
|
@ -241,6 +236,8 @@ class DownloadWorker(
|
|||
|
||||
if (logDownloads){
|
||||
logRepo.update(it.out, logItem.id)
|
||||
}else{
|
||||
logRepo.delete(logItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
9
app/src/main/res/layout/already_exists_card.xml
Normal file
9
app/src/main/res/layout/already_exists_card.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout android:id="@+id/existing_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
43
app/src/main/res/layout/already_exists_item.xml
Normal file
43
app/src/main/res/layout/already_exists_item.xml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?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"
|
||||
android:id="@+id/playlist_card_constraintLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:checkable="true"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="5dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/already_exists_title"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:gravity="start"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginEnd="10dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/already_exists_edit"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/already_exists_edit"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_edit"
|
||||
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/already_exists_title" />
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
68
app/src/main/res/layout/command_templates_sort_sheet.xml
Normal file
68
app/src/main/res/layout/command_templates_sort_sheet.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout style="@style/Widget.Material3.BottomSheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/sort_by"
|
||||
android:textSize="12sp"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingVertical="10dp"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:drawablePadding="30dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15sp"
|
||||
android:text="@string/date_added"
|
||||
app:drawableStartCompat="@drawable/ic_arrow_up" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingVertical="10dp"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:drawablePadding="30dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15sp"
|
||||
android:text="@string/title"
|
||||
app:drawableStartCompat="@drawable/ic_arrow_up" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/length"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingVertical="10dp"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:drawablePadding="30dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15sp"
|
||||
android:text="@string/length"
|
||||
app:drawableStartCompat="@drawable/ic_arrow_up" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
61
app/src/main/res/layout/filename_template_dialog.xml
Normal file
61
app/src/main/res/layout/filename_template_dialog.xml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:padding="10dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/filename"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/filename_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/suggested"
|
||||
android:layout_width="0dp"
|
||||
android:padding="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/filename"
|
||||
android:text="@string/suggested" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintHeight_max="200dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/suggested">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/filename_suggested_chipgroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:chipSpacingVertical="-7dp"
|
||||
app:chipSpacingHorizontal="5dp"
|
||||
app:selectionRequired="false"
|
||||
app:singleSelection="false">
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -34,6 +34,22 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/format_loading_progress"
|
||||
android:visibility="gone"
|
||||
android:layout_width="0dp"
|
||||
android:indeterminate="true"
|
||||
app:trackColor="#000"
|
||||
android:layout_gravity="bottom"
|
||||
android:alpha="0.3"
|
||||
android:scaleY="200"
|
||||
app:layout_constraintTop_toTopOf="@id/container"
|
||||
app:layout_constraintBottom_toBottomOf="@id/container"
|
||||
app:layout_constraintStart_toStartOf="@id/container"
|
||||
app:layout_constraintEnd_toEndOf="@id/container"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="5dp"
|
||||
android:text="@string/video" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
@ -111,6 +112,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="5dp"
|
||||
android:text="@string/audio" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
|||
|
|
@ -22,66 +22,74 @@
|
|||
app:navigationIcon="@drawable/ic_back"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/chips_command"
|
||||
android:layout_width="wrap_content"
|
||||
android:scrollbars="none"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
<LinearLayout
|
||||
android:id="@+id/history_selection_chips"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:selectionRequired="false"
|
||||
app:singleSelection="false">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/newTemplate"
|
||||
android:id="@+id/sortChip"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/new_template"
|
||||
app:chipIcon="@drawable/ic_plus" />
|
||||
android:text="@string/sort_by"
|
||||
app:chipIcon="@drawable/ic_down" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/shortcuts"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/shortcuts"
|
||||
app:chipIcon="@drawable/ic_shortcut" />
|
||||
<View style="@style/Divider.Vertical"/>
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="match_parent"
|
||||
app:selectionRequired="false"
|
||||
app:singleSelection="true"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</LinearLayout>
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/newTemplate"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/new_template"
|
||||
app:chipIcon="@drawable/ic_plus" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/shortcuts"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/shortcuts"
|
||||
app:chipIcon="@drawable/ic_shortcut" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_below="@+id/chips_command"
|
||||
android:id="@+id/template_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
>
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id="@+id/template_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
>
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
|
||||
<include layout="@layout/history_no_results"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout style="@style/Widget.Material3.BottomSheet"
|
||||
<FrameLayout xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.Material3.BottomSheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
|
||||
|
|
@ -17,10 +18,11 @@
|
|||
android:padding="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toStartOf="@+id/downloads_download_button_layout"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_button_type"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
|
@ -43,28 +45,19 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/downloads_download_button_layout"
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/download_button_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:clickable="false"
|
||||
android:elevation="0dp"
|
||||
app:borderWidth="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:elevation="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/downloads_download_button_type"
|
||||
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="55dp"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:clickable="false"
|
||||
android:elevation="0dp"
|
||||
app:borderWidth="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:elevation="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:contentDescription="@string/share" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/webview_appbarlayout"
|
||||
app:liftOnScroll="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:elevation="0dp"
|
||||
app:elevation="0dp"
|
||||
android:fitsSystemWindows="true"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinatorLayout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/webviewToolbar"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/webview_appbarlayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator"
|
||||
>
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:elevation="0dp"
|
||||
android:fitsSystemWindows="true"
|
||||
app:elevation="0dp"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<Button
|
||||
android:id="@+id/generate"
|
||||
android:layout_gravity="end"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:text="@string/ok"
|
||||
app:icon="@drawable/ic_check"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="PrivateResource" />
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/webviewToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator">
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
<Button
|
||||
android:id="@+id/generate"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:autoLink="all"
|
||||
android:text="@string/ok"
|
||||
app:icon="@drawable/ic_check"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="PrivateResource" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/rl"
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/webview_compose"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
android:layout_height="match_parent">
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/coordinatorLayout2"
|
||||
tools:composableName="com.deniscerri.ytdlnis.ui.more.WebViewActivity.WebViewView" />
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/webview_compose"
|
||||
tools:composableName="com.deniscerri.ytdlnis.ui.more.WebViewActivity.WebViewView"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/search_command"
|
||||
android:title="@string/search"
|
||||
android:icon="@drawable/ic_search"
|
||||
app:showAsAction="collapseActionView|ifRoom"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/import_clipboard"
|
||||
android:title="@string/import_from_clipboard"
|
||||
|
|
|
|||
|
|
@ -100,13 +100,7 @@
|
|||
android:id="@+id/resultCardDetailsDialog"
|
||||
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
|
||||
android:label="ResultCardDetailsDialog" />
|
||||
<navigation android:id="@+id/settingsNav"
|
||||
app:startDestination="@id/settingsFrame">
|
||||
<fragment
|
||||
android:id="@+id/settingsFrame"
|
||||
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsFragmentFrame"
|
||||
android:label="settings_frame"
|
||||
tools:layout="@layout/fragment_settings" />
|
||||
<navigation android:id="@+id/settingsNav">
|
||||
<fragment
|
||||
android:id="@+id/folderSettingsFragment"
|
||||
android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment"
|
||||
|
|
|
|||
|
|
@ -821,4 +821,111 @@
|
|||
<item>m4a|mp4a|aac</item>
|
||||
<item>opus</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="filename_templates">
|
||||
<item>%(uploader)s___(string): Full name of the video uploader)</item>
|
||||
<item>%(title)s___(string): Video title)</item>
|
||||
<item>%(id)s___(string): Video identifier)</item>
|
||||
<item>%(fulltitle)s___(string): Video title ignoring live timestamp and generic title)</item>
|
||||
<item>%(ext)s___(string): Video filename extension)</item>
|
||||
<item>%(alt_title)s___(string): A secondary title of the video)</item>
|
||||
<item>%(description)s___(string): The description of the video)</item>
|
||||
<item>%(display_id)s___(string): An alternative identifier for the video)</item>
|
||||
<item>%(license)s___(string): License name the video is licensed under)</item>
|
||||
<item>%(creator)s___(string): The creator of the video)</item>
|
||||
<item>%(timestamp)s___(numeric): UNIX timestamp of the moment the video became available)</item>
|
||||
<item>%(upload_date)s___(string): Video upload date in UTC (YYYYMMDD))</item>
|
||||
<item>%(release_timestamp)s___(numeric): UNIX timestamp of the moment the video was released)</item>
|
||||
<item>%(release_date)s___(string): The date (YYYYMMDD) when the video was released in UTC)</item>
|
||||
<item>%(modified_timestamp)s___(numeric): UNIX timestamp of the moment the video was last modified)</item>
|
||||
<item>%(modified_date)s___(string): The date (YYYYMMDD) when the video was last modified in UTC)</item>
|
||||
<item>%(uploader_id)s___(string): Nickname or id of the video uploader)</item>
|
||||
<item>%(channel)s___(string): Full name of the channel the video is uploaded on)</item>
|
||||
<item>%(channel_id)s___(string): Id of the channel)</item>
|
||||
<item>%(channel_follower_count)s___(numeric): Number of followers of the channel)</item>
|
||||
<item>%(channel_is_verified)s___(boolean): Whether the channel is verified on the platform)</item>
|
||||
<item>%(location)s___(string): Physical location where the video was filmed)</item>
|
||||
<item>%(duration)s___(numeric): Length of the video in seconds)</item>
|
||||
<item>%(duration_string)s___(string): Length of the video (HH:mm:ss))</item>
|
||||
<item>%(view_count)s___(numeric): How many users have watched the video on the platform)</item>
|
||||
<item>%(concurrent_view_count)s___(numeric): How many users are currently watching the video on the platform.)</item>
|
||||
<item>%(like_count)s___(numeric): Number of positive ratings of the video)</item>
|
||||
<item>%(dislike_count)s___(numeric): Number of negative ratings of the video)</item>
|
||||
<item>%(repost_count)s___(numeric): Number of reposts of the video)</item>
|
||||
<item>%(average_rating)s___(numeric): Average rating give by users, the scale used depends on the webpage)</item>
|
||||
<item>%(comment_count)s___(numeric): Number of comments on the video (For some extractors, comments are only downloaded at the end, and so this field cannot be used))</item>
|
||||
<item>%(age_limit)s___(numeric): Age restriction for the video (years))</item>
|
||||
<item>%(live_status)s___(string): One of "not_live", "is_live", "is_upcoming", "was_live", "post_live" (was live, but VOD is not yet processed))</item>
|
||||
<item>%(is_live)s___(boolean): Whether this video is a live stream or a fixed-length video)</item>
|
||||
<item>%(was_live)s___(boolean): Whether this video was originally a live stream)</item>
|
||||
<item>%(playable_in_embed)s___(string): Whether this video is allowed to play in embedded players on other sites)</item>
|
||||
<item>%(availability)s___(string): Whether the video is "private", "premium_only", "subscriber_only", "needs_auth", "unlisted" or "public")</item>
|
||||
<item>%(start_time)s___(numeric): Time in seconds where the reproduction should start, as specified in the URL)</item>
|
||||
<item>%(end_time)s___(numeric): Time in seconds where the reproduction should end, as specified in the URL)</item>
|
||||
<item>%(extractor)s___(string): Name of the extractor)</item>
|
||||
<item>%(extractor_key)s___(string): Key name of the extractor)</item>
|
||||
<item>%(epoch)s___(numeric): Unix epoch of when the information extraction was completed)</item>
|
||||
<item>%(autonumber)s___(numeric): Number that will be increased with each download, starting at --autonumber-start, padded with leading zeros to 5 digits)</item>
|
||||
<item>%(video_autonumber)s___(numeric): Number that will be increased with each video)</item>
|
||||
<item>%(n_entries)s___(numeric): Total number of extracted items in the playlist)</item>
|
||||
<item>%(playlist_id)s___(string): Identifier of the playlist that contains the video)</item>
|
||||
<item>%(playlist_title)s___(string): Name of the playlist that contains the video)</item>
|
||||
<item>%(playlist)s___(string): playlist_id or playlist_title)</item>
|
||||
<item>%(playlist_count)s___(numeric): Total number of items in the playlist. May not be known if entire playlist is not extracted)</item>
|
||||
<item>%(playlist_index)s___(numeric): Index of the video in the playlist padded with leading zeros according the final index)</item>
|
||||
<item>%(playlist_autonumber)s___(numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist)</item>
|
||||
<item>%(playlist_uploader)s___(string): Full name of the playlist uploader)</item>
|
||||
<item>%(playlist_uploader_id)s___(string): Nickname or id of the playlist uploader)</item>
|
||||
<item>%(webpage_url)s___(string): A URL to the video webpage which if given to yt-dlp should allow to get the same result again)</item>
|
||||
<item>%(webpage_url_basename)s___(string): The basename of the webpage URL)</item>
|
||||
<item>%(webpage_url_domain)s___(string): The domain of the webpage URL)</item>
|
||||
<item>%(original_url)s___(string): The URL given by the user (or same as webpage_url for playlist entries))</item>
|
||||
<item>%(chapter)s___(string): Name or title of the chapter the video belongs to)</item>
|
||||
<item>%(chapter_number)s___(numeric): Number of the chapter the video belongs to)</item>
|
||||
<item>%(chapter_id)s___(string): Id of the chapter the video belongs to)</item>
|
||||
<item>%(series)s___(string): Title of the series or programme the video episode belongs to)</item>
|
||||
<item>%(season)s___(string): Title of the season the video episode belongs to)</item>
|
||||
<item>%(season_number)s___(numeric): Number of the season the video episode belongs to)</item>
|
||||
<item>%(season_id)s___(string): Id of the season the video episode belongs to)</item>
|
||||
<item>%(episode)s___(string): Title of the video episode)</item>
|
||||
<item>%(episode_number)s___(numeric): Number of the video episode within a season)</item>
|
||||
<item>%(episode_id)s___(string): Id of the video episode)</item>
|
||||
<item>%(track)s___(string): Title of the track)</item>
|
||||
<item>%(track_number)s___(numeric): Number of the track within an album or a disc)</item>
|
||||
<item>%(track_id)s___(string): Id of the track)</item>
|
||||
<item>%(artist)s___(string): Artist(s) of the track)</item>
|
||||
<item>%(genre)s___(string): Genre(s) of the track)</item>
|
||||
<item>%(album)s___(string): Title of the album the track belongs to)</item>
|
||||
<item>%(album_type)s___(string): Type of the album)</item>
|
||||
<item>%(album_artist)s___(string): List of all artists appeared on the album)</item>
|
||||
<item>%(disc_number)s___(numeric): Number of the disc or other physical medium the track belongs to)</item>
|
||||
<item>%(release_year)s___(numeric): Year (YYYY) when the album was released)</item>
|
||||
<item>%(section_title)s___(string): Title of the chapter)</item>
|
||||
<item>%(section_number)s___(numeric): Number of the chapter within the file)</item>
|
||||
<item>%(section_start)s___(numeric): Start time of the chapter in seconds)</item>
|
||||
<item>%(section_end)s___(numeric): End time of the chapter in seconds)</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="modify_download_card">
|
||||
<item>@string/schedule</item>
|
||||
<item>@string/title</item>
|
||||
<item>@string/author</item>
|
||||
<item>@string/container</item>
|
||||
<item>@string/adjust_audio</item>
|
||||
<item>@string/adjust_video</item>
|
||||
<item>@string/adjust_templates</item>
|
||||
<item>@string/url</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="modify_download_card_values">
|
||||
<item>schedule</item>
|
||||
<item>title</item>
|
||||
<item>author</item>
|
||||
<item>container</item>
|
||||
<item>adjust_audio</item>
|
||||
<item>adjust_video</item>
|
||||
<item>adjust_templates</item>
|
||||
<item>url</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="confirm_delete_history_desc">Delete the entire history, permanently</string>
|
||||
<string name="confirm_delete_history">Confirm</string>
|
||||
<string name="search_history_hint">Search from History</string>
|
||||
<string name="search_command_hint">Search from Commands</string>
|
||||
<string name="no_results">No Results</string>
|
||||
<string name="more">More</string>
|
||||
<string name="update_app">Check for updates</string>
|
||||
|
|
@ -310,4 +311,11 @@
|
|||
<string name="remember_download_type">Remember Download Type</string>
|
||||
<string name="remember_download_type_summary">Select the last opened download type for the current item</string>
|
||||
<string name="user_agent_header">User-Agent header</string>
|
||||
<string name="length">Length</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="license">License</string>
|
||||
<string name="download_already_exists_summary">Do you still want to download?</string>
|
||||
<string name="schedule">Schedule</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="modify_download_card">Modify Download Card</string>
|
||||
</resources>
|
||||
|
|
@ -37,6 +37,13 @@
|
|||
app:title="@string/download_over_metered_networks" />
|
||||
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
android:icon="@drawable/baseline_cookie_24"
|
||||
android:key="use_cookies"
|
||||
app:title="@string/use_cookies" />
|
||||
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/baseline_network_locked_24"
|
||||
app:key="proxy"
|
||||
|
|
@ -111,10 +118,14 @@
|
|||
|
||||
<EditTextPreference
|
||||
app:key="retries"
|
||||
android:defaultValue=""
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:title="@string/retries" />
|
||||
|
||||
<EditTextPreference
|
||||
app:key="fragment_retries"
|
||||
android:defaultValue=""
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:title="@string/fragment_retries" />
|
||||
</PreferenceCategory>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,19 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
android:defaultValue=""
|
||||
app:icon="@drawable/ic_music_downloaded"
|
||||
app:key="music_path"
|
||||
app:title="@string/music_directory" />
|
||||
|
||||
<Preference
|
||||
android:defaultValue=""
|
||||
app:icon="@drawable/ic_video_downloaded"
|
||||
app:key="video_path"
|
||||
app:title="@string/video_directory" />
|
||||
|
||||
<Preference
|
||||
android:defaultValue=""
|
||||
app:icon="@drawable/ic_terminal"
|
||||
app:key="command_path"
|
||||
app:title="@string/command_directory" />
|
||||
|
|
@ -43,14 +46,14 @@
|
|||
|
||||
|
||||
|
||||
<EditTextPreference
|
||||
<Preference
|
||||
android:icon="@drawable/ic_textformat"
|
||||
app:key="file_name_template"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:defaultValue="%(uploader)s - %(title)s"
|
||||
app:title="@string/file_name_template" />
|
||||
|
||||
<EditTextPreference
|
||||
<Preference
|
||||
android:icon="@drawable/ic_textformat"
|
||||
app:key="file_name_template_audio"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@
|
|||
app:key="hide_thumbnails"
|
||||
app:title="@string/hide_thumbnails" />
|
||||
|
||||
<MultiSelectListPreference
|
||||
app:icon="@drawable/ic_card"
|
||||
android:dialogTitle="@string/modify_download_card"
|
||||
android:entries="@array/modify_download_card"
|
||||
android:entryValues="@array/modify_download_card_values"
|
||||
android:defaultValue="@array/modify_download_card_values"
|
||||
app:key="modify_download_card"
|
||||
app:title="@string/modify_download_card" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
app:title="@string/video_format" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue=""
|
||||
android:defaultValue="avc|h264"
|
||||
android:entries="@array/video_codec"
|
||||
android:entryValues="@array/video_codec_values"
|
||||
android:icon="@drawable/ic_code"
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
app:title="@string/preferred_video_codec" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue=""
|
||||
android:defaultValue="m4a|mp4a|aac"
|
||||
android:entries="@array/audio_codec"
|
||||
android:entryValues="@array/audio_codec_values"
|
||||
android:icon="@drawable/ic_code"
|
||||
|
|
@ -211,9 +211,4 @@
|
|||
android:summary="@string/use_extra_commands_summary"
|
||||
app:title="@string/use_extra_commands" />
|
||||
|
||||
<Preference
|
||||
app:isPreferenceVisible="false"
|
||||
android:key="use_cookies"
|
||||
android:defaultValue="false" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
app:icon="@drawable/baseline_security_24"
|
||||
app:key="security"
|
||||
app:summary="🔐 YTDLnis - Privacy Policy"
|
||||
app:title="SECURITY">
|
||||
app:title="@string/security">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/deniscerri/ytdlnis/blob/main/SECURITY.md" />
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
app:icon="@drawable/baseline_draw_24"
|
||||
app:key="license"
|
||||
app:summary="GNU General Public License v3.0"
|
||||
app:title="LICENSE">
|
||||
app:title="@string/license">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/deniscerri/ytdlnis/blob/main/LICENSE" />
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@
|
|||
app:summary="@string/update_app_summary"
|
||||
app:title="@string/update_app" />
|
||||
|
||||
<EditTextPreference
|
||||
app:isPreferenceVisible="false"
|
||||
app:key="skip_updates"
|
||||
app:defaultValue="" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ buildscript {
|
|||
coroutineVer = '1.7.3'
|
||||
retrofitVer = "2.9.0"
|
||||
kodeinVer = "7.16.0"
|
||||
navVer = "2.6.0"
|
||||
navVer = "2.7.4"
|
||||
media3_version = "1.1.1"
|
||||
agp_version = '8.1.0'
|
||||
agp_version1 = '7.4.2'
|
||||
|
|
@ -34,11 +34,11 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '7.4.2' apply false
|
||||
id 'com.android.library' version '7.4.2' apply false
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
id 'com.android.library' version '8.1.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false
|
||||
id 'com.android.test' version '7.4.2' apply false
|
||||
id 'com.android.test' version '8.1.1' apply false
|
||||
id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
|
||||
}
|
||||
|
||||
|
|
|
|||
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
#Sat Apr 08 15:57:50 CEST 2023
|
||||
#Wed Oct 18 18:36:39 CEST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
Loading…
Reference in a new issue