implemented cookies import & invert selected in contextual app bar & download preference without metered networks

This commit is contained in:
deniscerri 2023-04-12 22:17:33 +02:00
parent f6e5f771f6
commit 55b59d75b1
No known key found for this signature in database
GPG key ID: 95C43D517D830350
38 changed files with 521 additions and 305 deletions

View file

@ -13,6 +13,8 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" /> <uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".App" android:name=".App"

View file

@ -101,11 +101,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
cookieViewModel.updateCookiesFile()
cookieViewModel.getCookiesFromDB().getOrNull()?.let{
File(cacheDir, "cookies.txt").apply { writeText(it) }
}
val intent = intent val intent = intent
handleIntents(intent) handleIntents(intent)
} }

View file

@ -44,7 +44,7 @@ class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity
title.text = item?.url title.text = item?.url
val content = card.findViewById<TextView>(R.id.content) val content = card.findViewById<TextView>(R.id.content)
content.visibility = View.GONE content.text = item?.content
card.setOnClickListener { card.setOnClickListener {
onItemClickListener.onItemClick(item!!) onItemClickListener.onItemClick(item!!)

View file

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -172,6 +173,16 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
checkedItems.clear() checkedItems.clear()
} }
fun invertSelected(items: List<HistoryItem?>?){
val invertedList = mutableListOf<Long>()
items?.forEach {
if (!checkedItems.contains(it!!.id)) invertedList.add(it.id)
}
checkedItems.clear()
checkedItems.addAll(invertedList)
notifyDataSetChanged()
}
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<HistoryItem> = object : DiffUtil.ItemCallback<HistoryItem>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<HistoryItem> = object : DiffUtil.ItemCallback<HistoryItem>() {
override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean {

View file

@ -173,6 +173,16 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
notifyDataSetChanged() notifyDataSetChanged()
} }
fun invertSelected(items: List<ResultItem?>?){
val invertedList = mutableListOf<String>()
items?.forEach {
if (!checkedVideos.contains(it!!.url)) invertedList.add(it.url)
}
checkedVideos.clear()
checkedVideos.addAll(invertedList)
notifyDataSetChanged()
}
fun clearCheckedItems(){ fun clearCheckedItems(){
checkedVideos.clear() checkedVideos.clear()
} }

View file

@ -74,7 +74,7 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
} }
override fun areContentsTheSame(oldItem: CommandTemplate, newItem: CommandTemplate): Boolean { override fun areContentsTheSame(oldItem: CommandTemplate, newItem: CommandTemplate): Boolean {
return oldItem.content == newItem.content return oldItem.title == newItem.title && oldItem.content == newItem.content
} }
} }
} }

View file

@ -13,11 +13,11 @@ interface CookieDao {
@Query("SELECT * FROM cookies ORDER BY id DESC") @Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookiesLiveData() : LiveData<List<CookieItem>> fun getAllCookiesLiveData() : LiveData<List<CookieItem>>
@Query("SELECT * FROM cookies WHERE url=:url") @Query("SELECT EXISTS(SELECT * FROM cookies WHERE url=:url LIMIT 1)")
fun checkIfExistsWithSameURL(url: String) : CookieItem fun checkIfExistsWithSameURL(url: String) : Boolean
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: CookieItem) suspend fun insert(item: CookieItem) : Long
@Query("DELETE FROM cookies") @Query("DELETE FROM cookies")
suspend fun deleteAll() suspend fun deleteAll()

View file

@ -12,12 +12,11 @@ class CookieRepository(private val cookieDao: CookieDao) {
} }
suspend fun insert(item: CookieItem){ suspend fun insert(item: CookieItem) : Long{
try{ if (! cookieDao.checkIfExistsWithSameURL(item.url)){
cookieDao.checkIfExistsWithSameURL(item.url) return cookieDao.insert(item)
}catch(e: Exception){
cookieDao.insert(item)
} }
return -1
} }
suspend fun delete(item: CookieItem){ suspend fun delete(item: CookieItem){

View file

@ -6,26 +6,27 @@ import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.OPEN_READONLY import android.database.sqlite.SQLiteDatabase.OPEN_READONLY
import android.util.Log
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.database.repository.CookieRepository
import com.deniscerri.ytdlnis.ui.more.WebViewActivity import com.deniscerri.ytdlnis.ui.more.WebViewActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File
class CookieViewModel(private val application: Application) : AndroidViewModel(application) { class CookieViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: CookieRepository private val repository: CookieRepository
val items: LiveData<List<CookieItem>> val items: LiveData<List<CookieItem>>
private val jsonFormat = Json { prettyPrint = true } private val jsonFormat = Json { prettyPrint = true }
private val cookieHeader = "# Netscape HTTP Cookie File\n" +
"# WebView Generated by the YTDLnis app\n"
init { init {
val dao = DBManager.getInstance(application).cookieDao val dao = DBManager.getInstance(application).cookieDao
@ -37,12 +38,13 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
return repository.getAll() return repository.getAll()
} }
fun insert(item: CookieItem) = viewModelScope.launch(Dispatchers.IO) { suspend fun insert(item: CookieItem) : Long {
repository.insert(item) return repository.insert(item)
} }
fun delete(item: CookieItem) = viewModelScope.launch(Dispatchers.IO) { fun delete(item: CookieItem) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item) repository.delete(item)
updateCookiesFile()
} }
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
@ -106,20 +108,53 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
close() close()
} }
close() close()
val header = "# Netscape HTTP Cookie File\n" + cookieList.fold(StringBuilder("")) { acc, cookie ->
"# WebView Generated by the YTDLnis app\n"
cookieList.fold(StringBuilder(header)) { acc, cookie ->
acc.append(cookie.toNetscapeFormat()).append("\n") acc.append(cookie.toNetscapeFormat()).append("\n")
}.toString() }.toString()
} }
} }
fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) {
val cookies = repository.getAll()
val cookieTXT = StringBuilder(cookieHeader)
val cookieFile = File(application.cacheDir, "cookies.txt")
if (cookies.isEmpty()) cookieFile.apply { writeText("") }
cookies.forEach {
it.content.lines().forEach {line ->
if (! cookieTXT.contains(line)) cookieTXT.append(it.content)
}
}
cookieFile.apply { writeText(cookieTXT.toString()) }
}
suspend fun importFromClipboard() {
try{
val clipboard: ClipboardManager =
application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
var clip = clipboard.primaryClip!!.getItemAt(0).text
Log.e("Aaa", clip.toString())
if (clip.startsWith(cookieHeader)){
clip = clip.removePrefix(cookieHeader)
val cookie = CookieItem(
0,
"Cookie Import [${System.currentTimeMillis()}]",
clip.toString()
)
insert(cookie)
updateCookiesFile()
}
}catch (e: Exception){
e.printStackTrace()
}
}
fun exportToClipboard() = viewModelScope.launch { fun exportToClipboard() = viewModelScope.launch {
try{ try{
val clipboard: ClipboardManager = val clipboard: ClipboardManager =
application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val cookieFile = File(application.cacheDir, "cookies.txt")
getCookiesFromDB().getOrNull()?.let { if (! cookieFile.exists()) updateCookiesFile()
cookieFile.readText().let {
clipboard.setText(it) clipboard.setText(it)
} }
}catch (e: Exception){ }catch (e: Exception){

View file

@ -2,7 +2,10 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
@ -10,10 +13,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.work.Data import androidx.work.*
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
@ -287,6 +287,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
suspend fun queueDownloads(items: List<DownloadItem>) { suspend fun queueDownloads(items: List<DownloadItem>) {
val context = getApplication<App>().applicationContext val context = getApplication<App>().applicationContext
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
items.forEach { it.status = DownloadRepository.Status.Queued.toString() } items.forEach { it.status = DownloadRepository.Status.Queued.toString() }
val ids = repository.insertAll(items) val ids = repository.insertAll(items)
for (i in ids.indices){ for (i in ids.indices){
@ -301,6 +302,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(Data.Builder().putLong("id", it.id).build()) .setInputData(Data.Builder().putLong("id", it.id).build())
.addTag("download") .addTag("download")
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(if (allowMeteredNetworks) NetworkType.CONNECTED else NetworkType.UNMETERED)
.build()
)
.setInitialDelay(delay, TimeUnit.MILLISECONDS) .setInitialDelay(delay, TimeUnit.MILLISECONDS)
.build() .build()
@ -318,6 +324,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
repository.delete(it) repository.delete(it)
} }
} }
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}
} }
} }

View file

@ -78,10 +78,7 @@ class ShareActivity : AppCompatActivity() {
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java] cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
sharedPreferences = getSharedPreferences("root_preferences", Activity.MODE_PRIVATE) sharedPreferences = getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
cookieViewModel.getCookiesFromDB().getOrNull()?.let{ cookieViewModel.updateCookiesFile()
File(cacheDir, "cookies.txt").apply { writeText(it) }
}
val intent = intent val intent = intent
handleIntents(intent) handleIntents(intent)
} }

View file

@ -52,6 +52,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickListener { class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickListener {
@ -595,6 +596,17 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
R.id.invert_selected -> {
homeAdapter?.invertSelected(resultsList)
val invertedList = arrayListOf<ResultItem>()
resultsList?.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it!!)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
true
}
else -> false else -> false
} }
} }

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloads
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -32,6 +33,7 @@ import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.HistoryAdapter import com.deniscerri.ytdlnis.adapter.HistoryAdapter
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@ -213,6 +215,10 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
deleteDialog.show() deleteDialog.show()
} }
} }
R.id.download_queue -> {
val intent = Intent(context, DownloadQueueActivity::class.java)
startActivity(intent)
}
R.id.remove_deleted_history -> { R.id.remove_deleted_history -> {
if(allhistoryList!!.isEmpty()){ if(allhistoryList!!.isEmpty()){
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
@ -565,6 +571,17 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
actionMode?.finish() actionMode?.finish()
true true
} }
R.id.invert_selected -> {
historyAdapter?.invertSelected(historyList)
val invertedList = arrayListOf<HistoryItem>()
historyList?.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it!!)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
true
}
else -> false else -> false
} }
} }

View file

@ -4,13 +4,16 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
@ -22,6 +25,8 @@ import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.net.CookieManager import java.net.CookieManager
@ -70,13 +75,18 @@ class CookiesActivity : AppCompatActivity(), CookieAdapter.OnItemClickListener {
deleteDialog.setMessage(getString(R.string.confirm_delete_cookies_desc)) deleteDialog.setMessage(getString(R.string.confirm_delete_cookies_desc))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
android.webkit.CookieManager.getInstance().removeAllCookies(null) cookiesViewModel.deleteAll()
kotlin.runCatching { kotlin.runCatching {
File(cacheDir, "cookies.txt").apply { writeText("") } File(cacheDir, "cookies.txt").apply { writeText("") }
} }
} }
deleteDialog.show() deleteDialog.show()
} }
R.id.import_clipboard -> {
lifecycleScope.launch(Dispatchers.IO){
cookiesViewModel.importFromClipboard()
}
}
R.id.export_clipboard -> { R.id.export_clipboard -> {
cookiesViewModel.exportToClipboard() cookiesViewModel.exportToClipboard()
} }
@ -97,7 +107,9 @@ class CookiesActivity : AppCompatActivity(), CookieAdapter.OnItemClickListener {
builder.setTitle(getString(R.string.cookies)) builder.setTitle(getString(R.string.cookies))
val inputLayout = layoutInflater.inflate(R.layout.textinput, null) val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext) val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
editText.setText(url) val text = if (url.isNullOrBlank()) "https://" else url
editText.setText(text)
editText.setSelection(editText.text.length)
builder.setView(inputLayout) builder.setView(inputLayout)
builder.setPositiveButton( builder.setPositiveButton(
getString(R.string.get_cookies) getString(R.string.get_cookies)
@ -112,9 +124,8 @@ class CookiesActivity : AppCompatActivity(), CookieAdapter.OnItemClickListener {
getString(R.string.cancel) getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> } ) { dialog: DialogInterface?, which: Int -> }
val dialog = builder.create() val dialog = builder.create()
editText.setOnKeyListener { view, i, keyEvent -> editText.doOnTextChanged { text, start, before, count ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
false
} }
dialog.show() dialog.show()
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
@ -7,12 +8,16 @@ import android.webkit.WebViewClient
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -25,6 +30,7 @@ class WebViewActivity : AppCompatActivity() {
private lateinit var url: String private lateinit var url: String
private lateinit var cookies: String private lateinit var cookies: String
@SuppressLint("SetJavaScriptEnabled")
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.webview_activity) setContentView(R.layout.webview_activity)
@ -35,6 +41,7 @@ class WebViewActivity : AppCompatActivity() {
toolbar = appbar.findViewById(R.id.webviewToolbar) toolbar = appbar.findViewById(R.id.webviewToolbar)
generateBtn = toolbar.findViewById(R.id.generate) generateBtn = toolbar.findViewById(R.id.generate)
cookieManager = CookieManager.getInstance() cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
webView = findViewById(R.id.webview) webView = findViewById(R.id.webview)
webView.settings.javaScriptEnabled = true webView.settings.javaScriptEnabled = true
@ -62,20 +69,24 @@ class WebViewActivity : AppCompatActivity() {
generateBtn.setOnClickListener { generateBtn.setOnClickListener {
cookiesViewModel.getCookiesFromDB().getOrNull()?.let { cookiesViewModel.getCookiesFromDB().getOrNull()?.let {
File(cacheDir , "/cookies.txt").apply { writeText(it) } kotlin.runCatching {
lifecycleScope.launch {
withContext(Dispatchers.IO){
cookiesViewModel.insert(
CookieItem(
0,
url,
it
)
)
}
cookiesViewModel.updateCookiesFile()
}
}.onFailure {
Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show()
}
} }
kotlin.runCatching { cookieManager.removeAllCookies(null)
cookiesViewModel.insert(
CookieItem(
0,
url,
""
)
)
}.onFailure {
Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show()
}
cookieManager.flush()
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }

View file

@ -43,6 +43,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var incognito: SwitchPreferenceCompat? = null private var incognito: SwitchPreferenceCompat? = null
private var preferredDownloadType : ListPreference? = null private var preferredDownloadType : ListPreference? = null
private var downloadCard: SwitchPreferenceCompat? = null private var downloadCard: SwitchPreferenceCompat? = null
private var meteredNetwork: SwitchPreferenceCompat? = null
private var apiKey: EditTextPreference? = null private var apiKey: EditTextPreference? = null
private var concurrentFragments: SeekBarPreference? = null private var concurrentFragments: SeekBarPreference? = null
private var concurrentDownloads: SeekBarPreference? = null private var concurrentDownloads: SeekBarPreference? = null
@ -106,6 +107,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
incognito = findPreference("incognito") incognito = findPreference("incognito")
preferredDownloadType = findPreference("preferred_download_type") preferredDownloadType = findPreference("preferred_download_type")
downloadCard = findPreference("download_card") downloadCard = findPreference("download_card")
meteredNetwork = findPreference("metered_networks")
apiKey = findPreference("api_key") apiKey = findPreference("api_key")
concurrentFragments = findPreference("concurrent_fragments") concurrentFragments = findPreference("concurrent_fragments")
concurrentDownloads = findPreference("concurrent_downloads") concurrentDownloads = findPreference("concurrent_downloads")
@ -168,6 +170,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
editor.putBoolean("incognito", incognito!!.isChecked) editor.putBoolean("incognito", incognito!!.isChecked)
editor.putString("preferred_download_type", preferredDownloadType!!.value) editor.putString("preferred_download_type", preferredDownloadType!!.value)
editor.putBoolean("download_card", downloadCard!!.isChecked) editor.putBoolean("download_card", downloadCard!!.isChecked)
editor.putBoolean("metered_networks", meteredNetwork!!.isChecked)
editor.putString("api_key", apiKey!!.text) editor.putString("api_key", apiKey!!.text)
editor.putInt("concurrent_fragments", concurrentFragments!!.value) editor.putInt("concurrent_fragments", concurrentFragments!!.value)
editor.putInt("concurrent_downloads", concurrentDownloads!!.value) editor.putInt("concurrent_downloads", concurrentDownloads!!.value)
@ -282,8 +285,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
editor.apply() editor.apply()
true true
} }
downloadCard!!.onPreferenceChangeListener = downloadCard!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val enable = newValue as Boolean val enable = newValue as Boolean
@ -291,6 +292,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
editor.apply() editor.apply()
true true
} }
meteredNetwork!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val enable = newValue as Boolean
editor.putBoolean("metered_networks", enable)
editor.apply()
true
}
apiKey!!.onPreferenceChangeListener = apiKey!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putString("api_key", newValue.toString()) editor.putString("api_key", newValue.toString())

View file

@ -3,7 +3,9 @@ package com.deniscerri.ytdlnis.util
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
@ -138,63 +140,70 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple { fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple {
init() try{
items = ArrayList() init()
if (key!!.isEmpty()) { items = ArrayList()
return if (useInvidous) getPlaylistFromInvidous(id) else PlaylistTuple( if (key!!.isEmpty()) {
return if (useInvidous) getPlaylistFromInvidous(id) else PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
}
val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50&regionCode=$countryCODE&playlistId=$id&key=$key"
//short data
val res = genericRequest(url)
if (!res.has("items")) return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
val dataArray = res.getJSONArray("items")
//extra data
var url2 = "https://www.googleapis.com/youtube/v3/videos?id="
//getting all ids, for the extra data request
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
val videoID = snippet.getJSONObject("resourceId").getString("videoId")
url2 = "$url2$videoID,"
snippet.put("videoID", videoID)
}
url2 = url2.substring(
0,
url2.length - 1
) + "&part=contentDetails&regionCode=" + countryCODE + "&key=" + key
val extra = genericRequest(url2)
val extraArray = extra.getJSONArray("items")
var j = 0
var i = 0
while (i < extraArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
var duration =
extra.getJSONArray("items").getJSONObject(j).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
snippet.put("duration", duration)
fixThumbnail(snippet)
val v = createVideofromJSON(snippet)
if (v == null || v.thumb.isEmpty()) {
i++
continue
} else {
j++
}
v.playlistTitle = "YTDLnis"
items.add(v)
i++
}
val next = res.optString("nextPageToken")
return PlaylistTuple(next, items)
}catch (e: Exception){
return PlaylistTuple(
"", "",
getFromYTDL("https://www.youtube.com/playlist?list=$id") getFromYTDL("https://www.youtube.com/playlist?list=$id")
) )
} }
val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50&regionCode=$countryCODE&playlistId=$id&key=$key"
//short data
val res = genericRequest(url)
if (!res.has("items")) return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
val dataArray = res.getJSONArray("items")
//extra data
var url2 = "https://www.googleapis.com/youtube/v3/videos?id="
//getting all ids, for the extra data request
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
val videoID = snippet.getJSONObject("resourceId").getString("videoId")
url2 = "$url2$videoID,"
snippet.put("videoID", videoID)
}
url2 = url2.substring(
0,
url2.length - 1
) + "&part=contentDetails&regionCode=" + countryCODE + "&key=" + key
val extra = genericRequest(url2)
val extraArray = extra.getJSONArray("items")
var j = 0
var i = 0
while (i < extraArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
var duration =
extra.getJSONArray("items").getJSONObject(j).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
snippet.put("duration", duration)
fixThumbnail(snippet)
val v = createVideofromJSON(snippet)
if (v == null || v.thumb.isEmpty()) {
i++
continue
} else {
j++
}
v.playlistTitle = "YTDLnis"
items.add(v)
i++
}
val next = res.optString("nextPageToken")
return PlaylistTuple(next, items)
} }
private fun getPlaylistFromInvidous(id: String): PlaylistTuple { private fun getPlaylistFromInvidous(id: String): PlaylistTuple {
@ -221,30 +230,34 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getVideo(id: String): ResultItem? { fun getVideo(id: String): ResultItem? {
init() try {
if (key!!.isEmpty()) { init()
return if (useInvidous) { if (key!!.isEmpty()) {
val res = genericRequest(invidousURL + "videos/" + id) return if (useInvidous) {
if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideofromInvidiousJSON( val res = genericRequest(invidousURL + "videos/" + id)
res if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideofromInvidiousJSON(
) res
} else { )
getFromYTDL("https://www.youtube.com/watch?v=$id")[0] } else {
getFromYTDL("https://www.youtube.com/watch?v=$id")[0]
}
} }
}
//short data //short data
var res = var res =
genericRequest("https://youtube.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=$id&key=$key") genericRequest("https://youtube.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=$id&key=$key")
if (!res.has("items")) return getFromYTDL("https://www.youtube.com/watch?v=$id")[0] if (!res.has("items")) return getFromYTDL("https://www.youtube.com/watch?v=$id")[0]
var duration = res.getJSONArray("items").getJSONObject(0).getJSONObject("contentDetails") var duration = res.getJSONArray("items").getJSONObject(0).getJSONObject("contentDetails")
.getString("duration") .getString("duration")
duration = formatDuration(duration) duration = formatDuration(duration)
res = res.getJSONArray("items").getJSONObject(0).getJSONObject("snippet") res = res.getJSONArray("items").getJSONObject(0).getJSONObject("snippet")
res.put("videoID", id) res.put("videoID", id)
res.put("duration", duration) res.put("duration", duration)
fixThumbnail(res) fixThumbnail(res)
return createVideofromJSON(res) return createVideofromJSON(res)
}catch (e: Exception){
return getFromYTDL("https://www.youtube.com/watch?v=$id")[0]
}
} }
private fun createVideofromJSON(obj: JSONObject): ResultItem? { private fun createVideofromJSON(obj: JSONObject): ResultItem? {
@ -510,7 +523,8 @@ class InfoUtil(private val context: Context) {
arrayOf(youtubeDLResponse.out) arrayOf(youtubeDLResponse.out)
} }
for (result in results) { for (result in results) {
val jsonObject = JSONObject(result!!) if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
val title = if (jsonObject.has("title")) { val title = if (jsonObject.has("title")) {
if (jsonObject.getString("title") == "[Private video]") continue if (jsonObject.getString("title") == "[Private video]") continue
jsonObject.getString("title") jsonObject.getString("title")
@ -531,7 +545,9 @@ class InfoUtil(private val context: Context) {
thumb = jsonObject.getString("thumbnail") thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) { } else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails") val thumbs = jsonObject.getJSONArray("thumbnails")
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url") if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
} }
val website = if (jsonObject.has("ie_key")) jsonObject.getString("ie_key") else jsonObject.getString("extractor") val website = if (jsonObject.has("ie_key")) jsonObject.getString("ie_key") else jsonObject.getString("extractor")
var playlistTitle: String? = "" var playlistTitle: String? = ""
@ -560,6 +576,9 @@ class InfoUtil(private val context: Context) {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
e.printStackTrace() e.printStackTrace()
} }
return items return items

View file

@ -145,25 +145,27 @@ class DownloadWorker(
if (audioQualityId.isNotBlank()){ if (audioQualityId.isNotBlank()){
request.addOption("-f", audioQualityId) request.addOption("-f", audioQualityId)
}
val ext = downloadItem.format.container val ext = downloadItem.format.container
if(ext != context.getString(R.string.defaultValue) && ext != "webm"){ if(ext != context.getString(R.string.defaultValue) && ext != "webm"){
val codec = when(downloadItem.format.container){ val codec = when(downloadItem.format.container){
"m4a" -> "aac" "aac" -> "aac"
"aac" -> "aac" "mp3" -> "libmp3lame"
"mp3" -> "libmp3lame" "flac" -> "flac"
"flac" -> "flac" "opus" -> "libopus"
"opus" -> "libopus" else -> ""
else -> "" }
} Log.e("aa", codec)
Log.e("aa", codec) if (codec.isEmpty()){
if (codec.isEmpty()){ request.addOption("-x")
request.addOption("-x") }else{
}else{ request.addOption("--remux-video", ext)
request.addOption("--remux-video", ext) request.addOption("--ppa", "VideoRemuxer:-vn -c:a $codec")
request.addOption("--ppa", "VideoRemuxer:-vn -c:a $codec") }
} }
}else{
request.addOption("-x")
} }
request.addOption("--embed-metadata") request.addOption("--embed-metadata")

View file

@ -103,6 +103,15 @@ class TerminalDownloadWorker(
}, 1000) }, 1000)
} }
val chunks = it.out.chunked(5000)
val dataBuilder = Data.Builder();
dataBuilder.putString("output", chunks[0])
return Result.success(
dataBuilder.build()
)
}.onFailure { }.onFailure {
if (it is YoutubeDL.CanceledException) { if (it is YoutubeDL.CanceledException) {
return Result.failure() return Result.failure()

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:textColorPrimary" android:pathData="M18.32,4.26C16.84,3.05 15.01,2.25 13,2.05v2.02c1.46,0.18 2.79,0.76 3.9,1.62L18.32,4.26zM19.93,11h2.02c-0.2,-2.01 -1,-3.84 -2.21,-5.32L18.31,7.1C19.17,8.21 19.75,9.54 19.93,11zM18.31,16.9l1.43,1.43c1.21,-1.48 2.01,-3.32 2.21,-5.32h-2.02C19.75,14.46 19.17,15.79 18.31,16.9zM13,19.93v2.02c2.01,-0.2 3.84,-1 5.32,-2.21l-1.43,-1.43C15.79,19.17 14.46,19.75 13,19.93zM13,12V7h-2v5H7l5,5l5,-5H13zM11,19.93v2.02c-5.05,-0.5 -9,-4.76 -9,-9.95s3.95,-9.45 9,-9.95v2.02C7.05,4.56 4,7.92 4,12S7.05,19.44 11,19.93z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M18.99,11.5c0.34,0 0.67,0.03 1,0.07L20,0 0,20h11.56c-0.04,-0.33 -0.07,-0.66 -0.07,-1 0,-4.14 3.36,-7.5 7.5,-7.5zM22.7,19.49c0.02,-0.16 0.04,-0.32 0.04,-0.49 0,-0.17 -0.01,-0.33 -0.04,-0.49l1.06,-0.83c0.09,-0.08 0.12,-0.21 0.06,-0.32l-1,-1.73c-0.06,-0.11 -0.19,-0.15 -0.31,-0.11l-1.24,0.5c-0.26,-0.2 -0.54,-0.37 -0.85,-0.49l-0.19,-1.32c-0.01,-0.12 -0.12,-0.21 -0.24,-0.21h-2c-0.12,0 -0.23,0.09 -0.25,0.21l-0.19,1.32c-0.3,0.13 -0.59,0.29 -0.85,0.49l-1.24,-0.5c-0.11,-0.04 -0.24,0 -0.31,0.11l-1,1.73c-0.06,0.11 -0.04,0.24 0.06,0.32l1.06,0.83c-0.02,0.16 -0.03,0.32 -0.03,0.49 0,0.17 0.01,0.33 0.03,0.49l-1.06,0.83c-0.09,0.08 -0.12,0.21 -0.06,0.32l1,1.73c0.06,0.11 0.19,0.15 0.31,0.11l1.24,-0.5c0.26,0.2 0.54,0.37 0.85,0.49l0.19,1.32c0.02,0.12 0.12,0.21 0.25,0.21h2c0.12,0 0.23,-0.09 0.25,-0.21l0.19,-1.32c0.3,-0.13 0.59,-0.29 0.84,-0.49l1.25,0.5c0.11,0.04 0.24,0 0.31,-0.11l1,-1.73c0.06,-0.11 0.03,-0.24 -0.06,-0.32l-1.07,-0.83zM18.99,20.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View file

@ -24,7 +24,7 @@
android:checkable="true" android:checkable="true"
app:strokeWidth="0dp" app:strokeWidth="0dp"
app:cardPreventCornerOverlap="true" app:cardPreventCornerOverlap="true"
android:layout_margin="20dp"> android:layout_margin="10dp">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image_view" android:id="@+id/image_view"

View file

@ -29,7 +29,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:layout_margin="10dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
@ -37,6 +36,7 @@
android:id="@+id/chips_command" android:id="@+id/chips_command"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:layout_margin="10dp"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout

View file

@ -29,12 +29,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:layout_margin="10dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/newCookie" android:id="@+id/newCookie"
android:layout_margin="10dp"
style="@style/Widget.Material3.Chip.Assist" style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -30,47 +30,58 @@
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ScrollView <LinearLayout
android:layout_margin="10dp"
android:id="@+id/custom_command_scrollview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="0dp"
app:layout_constraintVertical_bias="0" android:orientation="horizontal"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout" app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<LinearLayout <ScrollView
android:id="@+id/custom_command_scrollview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:focusableInTouchMode="true" android:layout_margin="10dp">
android:orientation="vertical">
<TextView <LinearLayout
android:id="@+id/custom_command_output"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="monospace" android:focusableInTouchMode="true"
android:gravity="bottom" android:orientation="vertical">
android:textIsSelectable="true" />
<EditText <HorizontalScrollView
android:id="@+id/command_edittext" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent">
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fontFamily="monospace"
android:gravity="start"
android:inputType="textMultiLine"
android:paddingBottom="100dp"
android:maxLines="100"
android:text="yt-dlp "
android:textSize="15sp" />
</LinearLayout> <TextView
android:id="@+id/custom_command_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="bottom"
android:scrollbars="horizontal|vertical"
android:scrollHorizontally="true"
android:textIsSelectable="true" />
</ScrollView> </HorizontalScrollView>
<EditText
android:id="@+id/command_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fontFamily="monospace"
android:gravity="start"
android:inputType="textMultiLine"
android:maxLines="100"
android:text="yt-dlp "
android:textSize="15sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout

View file

@ -135,6 +135,8 @@
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/shortcutsChipGroup" android:id="@+id/shortcutsChipGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
app:chipSpacingVertical="-10dp"
app:chipSpacing="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:singleLine="false"> app:singleLine="false">

View file

@ -19,7 +19,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="10dp" android:paddingHorizontal="10dp"
android:paddingBottom="10dp"
android:hint="@string/title" android:hint="@string/title"
style="@style/Widget.Material3.TextInputLayout.FilledBox"> style="@style/Widget.Material3.TextInputLayout.FilledBox">
@ -34,7 +35,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="10dp" android:paddingHorizontal="10dp"
android:orientation="horizontal"> android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@ -87,8 +88,8 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/outputPath" android:id="@+id/outputPath"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:paddingTop="10dp"
android:paddingBottom="0dp" android:paddingBottom="0dp"
android:paddingTop="10dp"
style="@style/Widget.Material3.TextInputLayout.FilledBox" style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -113,7 +114,7 @@
<LinearLayout <LinearLayout
android:id="@+id/adjust_audio" android:id="@+id/adjust_audio"
android:layout_width="match_parent" android:layout_width="match_parent"
android:padding="10dp" android:paddingHorizontal="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
> >

View file

@ -19,7 +19,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:hint="@string/title" android:hint="@string/title"
android:padding="10dp"> android:paddingBottom="10dp"
android:paddingHorizontal="10dp">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
@ -32,7 +33,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="10dp"> android:paddingHorizontal="10dp">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/author_textinput" android:id="@+id/author_textinput"
@ -111,7 +112,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="10dp"> android:paddingHorizontal="10dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -6,132 +6,138 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:gravity="top"
android:orientation="vertical"
tools:ignore="MissingConstraints">
<ImageView <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/ic_app_icon"
android:scaleType="center" />
<TextView
android:id="@+id/terminal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/terminal" android:gravity="top"
android:textStyle="bold" android:orientation="vertical"
android:clickable="true" tools:ignore="MissingConstraints">
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp"
android:paddingVertical="15dp"
android:drawablePadding="35dp"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_terminal" />
<TextView <ImageView
android:id="@+id/logs" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_gravity="center"
android:text="@string/logs" android:background="@drawable/ic_app_icon"
android:textStyle="bold" android:scaleType="center" />
android:clickable="true"
android:focusable="true" <TextView
android:background="?android:attr/selectableItemBackground" android:id="@+id/terminal"
android:paddingHorizontal="15dp" android:layout_width="match_parent"
android:paddingVertical="15dp" android:layout_height="wrap_content"
android:drawablePadding="35dp" android:text="@string/terminal"
android:textSize="16sp" android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_baseline_file_open_24" /> android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp"
android:paddingVertical="15dp"
android:drawablePadding="35dp"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_terminal" />
<TextView
android:id="@+id/logs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/logs"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp"
android:paddingVertical="15dp"
android:drawablePadding="35dp"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_baseline_file_open_24" />
<TextView <TextView
android:id="@+id/command_templates" android:id="@+id/command_templates"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="35dp" android:drawablePadding="35dp"
android:textSize="16sp" android:textSize="16sp"
android:paddingVertical="15dp" android:paddingVertical="15dp"
android:text="@string/command_templates" android:text="@string/command_templates"
android:textStyle="bold" android:textStyle="bold"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/ic_baseline_keyboard_arrow_right_24" /> app:drawableStartCompat="@drawable/ic_baseline_keyboard_arrow_right_24" />
<TextView <TextView
android:id="@+id/download_queue" android:id="@+id/download_queue"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="35dp" android:drawablePadding="35dp"
android:textSize="16sp" android:textSize="16sp"
android:paddingVertical="15dp" android:paddingVertical="15dp"
android:text="@string/download_queue" android:text="@string/download_queue"
android:textStyle="bold" android:textStyle="bold"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/ic_concurrent_downloads" /> app:drawableStartCompat="@drawable/ic_concurrent_downloads" />
<TextView <TextView
android:id="@+id/cookies" android:id="@+id/cookies"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="35dp" android:drawablePadding="35dp"
android:textSize="16sp" android:textSize="16sp"
android:paddingVertical="15dp" android:paddingVertical="15dp"
android:text="@string/cookies" android:text="@string/cookies"
android:textStyle="bold" android:textStyle="bold"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/baseline_cookie_24" /> app:drawableStartCompat="@drawable/baseline_cookie_24" />
<TextView <TextView
android:id="@+id/terminate" android:id="@+id/terminate"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="35dp" android:drawablePadding="35dp"
android:textSize="16sp" android:textSize="16sp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingVertical="15dp" android:paddingVertical="15dp"
android:text="@string/kill_app" android:text="@string/kill_app"
android:textStyle="bold" android:textStyle="bold"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/ic_power" /> app:drawableStartCompat="@drawable/ic_power" />
<View <View
android:layout_margin="0dp" android:layout_margin="0dp"
style="@style/Divider.Horizontal" /> style="@style/Divider.Horizontal" />
<TextView <TextView
android:id="@+id/settings" android:id="@+id/settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="35dp" android:drawablePadding="35dp"
android:textSize="16sp" android:textSize="16sp"
android:paddingVertical="15dp" android:paddingVertical="15dp"
android:text="@string/settings" android:text="@string/settings"
android:textStyle="bold" android:textStyle="bold"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/ic_settings" /> app:drawableStartCompat="@drawable/ic_settings" />
</LinearLayout> </LinearLayout>
</ScrollView>
</RelativeLayout> </RelativeLayout>

View file

@ -10,15 +10,13 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp"
android:padding="10dp"
android:layout_marginVertical="10dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:text="@string/sort_by" android:text="@string/sort_by"
android:paddingBottom="20dp" android:textSize="12sp"
android:layout_margin="20dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
@ -27,6 +25,7 @@
android:focusable="true" android:focusable="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp" android:drawablePadding="30dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -41,6 +40,7 @@
android:focusable="true" android:focusable="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp" android:drawablePadding="30dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -55,6 +55,7 @@
android:focusable="true" android:focusable="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp" android:drawablePadding="30dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -70,6 +71,7 @@
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:drawablePadding="30dp" android:drawablePadding="30dp"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="15sp" android:textSize="15sp"
@ -77,7 +79,6 @@
app:drawableStartCompat="@drawable/ic_arrow_up" /> app:drawableStartCompat="@drawable/ic_arrow_up" />
</LinearLayout> </LinearLayout>

View file

@ -57,7 +57,8 @@
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/shortcutsChipGroup" android:id="@+id/shortcutsChipGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
app:chipSpacingVertical="0dp" app:chipSpacingVertical="-10dp"
app:chipSpacing="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:singleLine="false"> app:singleLine="false">

View file

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:padding="10dp"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout

View file

@ -6,6 +6,11 @@
android:title="@string/delete_all_cookies" android:title="@string/delete_all_cookies"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/import_clipboard"
android:title="@string/import_from_clipboard"
app:showAsAction="never" />
<item <item
android:id="@+id/export_clipboard" android:id="@+id/export_clipboard"
android:title="@string/export_from_clipboard" android:title="@string/export_from_clipboard"

View file

@ -7,9 +7,15 @@
android:id="@+id/search_history" android:id="@+id/search_history"
android:title="@string/search" android:title="@string/search"
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search"
app:showAsAction="collapseActionView|always" app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView"/> app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item
android:id="@+id/download_queue"
android:title="@string/download_queue"
android:icon="@drawable/baseline_downloading_24"
app:showAsAction="collapseActionView|ifRoom"/>
<item <item
android:id="@+id/remove_history" android:id="@+id/remove_history"
android:icon="@drawable/ic_delete_all" android:icon="@drawable/ic_delete_all"

View file

@ -15,5 +15,10 @@
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="@string/share" /> android:title="@string/share" />
<item
android:id="@+id/invert_selected"
android:title="@string/invert_selected"
app:showAsAction="never" />
</menu> </menu>

View file

@ -20,5 +20,10 @@
android:title="@string/select_all" android:title="@string/select_all"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/invert_selected"
android:title="@string/invert_selected"
app:showAsAction="never" />
</menu> </menu>

View file

@ -204,4 +204,8 @@
<string name="get_cookies">Get Cookies</string> <string name="get_cookies">Get Cookies</string>
<string name="delete_all_cookies">Delete All Cookies</string> <string name="delete_all_cookies">Delete All Cookies</string>
<string name="confirm_delete_cookies_desc">Delete all the cookies, permanently</string> <string name="confirm_delete_cookies_desc">Delete all the cookies, permanently</string>
<string name="download_over_metered_networks">Download over Metered Networks</string>
<string name="download_over_metered_networks_summary">Let the download worker work on any network type. If disabled, it will wait for an unmetered network to start the downloads</string>
<string name="metered_network_download_start_info">Download worker will start after you are connected to an unmetered network</string>
<string name="invert_selected">Invert Selected</string>
</resources> </resources>

View file

@ -65,6 +65,14 @@
app:summary="@string/download_card_summary" app:summary="@string/download_card_summary"
app:title="@string/show_download_card" /> app:title="@string/show_download_card" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/metered_networks"
android:key="metered_networks"
app:summary="@string/download_over_metered_networks_summary"
app:title="@string/download_over_metered_networks" />
<ListPreference <ListPreference
android:dependency="download_card" android:dependency="download_card"
android:defaultValue="video" android:defaultValue="video"